Lexical elements: Floating-point literals

January 18, 2023

Ah, who doesn’t love floating point numbers?

Go gives us two ways to define floating point number literals: in decimal, or in hexidecimal.

I have never been tempted to write a floating point number literal in hexidecimal. I imagine this is mostly used by those interested in precision control of the IEEE 754 values as understood by the floating point implementation. (If this is something you’ve ever used, I’d love to hear from you: What was your context?) The details are of course in the spec, for your perusal, but I’m going to focus on the decimial representation. Also, once you understand the details of the decimal notation, the hex notitation is easy-peasy.

Floating-point literals

A floating-point literal is a decimal or hexadecimal representation of a floating-point constant.

A decimal floating-point literal consists of an integer part (decimal digits), a decimal point, a fractional part (decimal digits), and an exponent part (e or E followed by an optional sign and decimal digits). One of the integer part or the fractional part may be elided; one of the decimal point or the exponent part may be elided. An exponent value exp scales the mantissa (integer and fractional part) by 10exp.

There’s a fair amount to unpack there. Let’s look at an example (borrowed from the quoted text below) to disect it a bit.

6.67428e-11

In this image, the integer part (6) is represented in red, the decimal point (.) in black, and the fractional part (67428) in blue.

This part should be familiar to virtually everyone. It’s just a decimal number with a fractional part. And for most purposes, this is sufficient when programming (in Go, or most other languages). Most human-scale numbers (distances traveled, money spent, weight lost, etc) are easily represented as integers or integers with a fractional part.

Of course we don’t only deal with human-scale numbers. Sometimes we have really big numbers, or really small numbers. And this is where that exponential notation comes into play.

The e character (alternately capitalized as E), indicates that the remainder of the floating-point literal is the exponent part. And this is interpreted as multiplying the preceeding part by 10 taken to the exponential power. Or in our case:

6.67428x10^-11

So this is a really small number. If we were to write it using standard notation it would be 0.0000000000667428. That’s easy to get wrong, by accidentially repeating or omitting a 0, for example.

Now that was a complete example. The spec explains that a few bits are optional, too. “One of the integer part or the fractional part may be elided.” This means that these are both valid expressions of floating-point literals:

.67428e-11
6.e-11

Then, "one of the decimal point or the exponent part may be elided".  This means our second example above can be simplified, by eliding the decimal place.

>```
> 6e-11
>```

And now for the sure hearted, here's what the spec has to say about hexidecimal floating-point literals:

> A hexadecimal floating-point literal consists of a `0x` or `0X` prefix, an integer part (hexadecimal digits), a radix point, a fractional part (hexadecimal digits), and an exponent part (`p` or `P` followed by an optional sign and decimal digits). One of the integer part or the fractional part may be elided; the radix point may be elided as well, but the exponent part is required. (This syntax matches the one given in IEEE 754-2008 §5.12.3.) An exponent value exp scales the mantissa (integer and fractional part) by 2<sup>exp</sup>.
>
> For readability, an underscore character \_ may appear after a base prefix or between successive digits; such underscores do not change the literal value.
>
> ```ebnf
> float_lit         = decimal_float_lit | hex_float_lit .
>
> decimal_float_lit = decimal_digits "." [ decimal_digits ] [ decimal_exponent ] |
>                     decimal_digits decimal_exponent |
>                     "." decimal_digits [ decimal_exponent ] .
> decimal_exponent  = ( "e" | "E" ) [ "+" | "-" ] decimal_digits .
>
> hex*float_lit = "0" ( "x" | "X" ) hex_mantissa hex_exponent .
> hex_mantissa = [ "*" ] hex*digits "." [ hex_digits ] |
>                [ "*" ] hex_digits |
>                "." hex_digits .
> hex_exponent = ( "p" | "P" ) [ "+" | "-" ] decimal_digits .
> ```
>
> ```
> 0.
> 72.40
> 072.40       // == 72.40
> 2.71828
> 1.e+0
> 6.67428e-11
> 1E6
> .25
> .12345E+5
> 1_5.         // == 15.0
> 0.15e+0_2    // == 15.0
>
> 0x1p-2       // == 0.25
> 0x2.p10      // == 2048.0
> 0x1.Fp+0     // == 1.9375
> 0X.8p-0      // == 0.5
> 0X_1FFFP-16  // == 0.1249847412109375
> 0x15e-2      // == 0x15e - 2 (integer subtraction)
>
> 0x.p1        // invalid: mantissa has no digits
> 1p-2         // invalid: p exponent requires hexadecimal mantissa
> 0x1.5e-2     // invalid: hexadecimal mantissa requires p exponent
> 1_.5         // invalid: _ must separate successive digits
> 1._5         // invalid: _ must separate successive digits
> 1.5_e1       // invalid: _ must separate successive digits
> 1.5e_1       // invalid: _ must separate successive digits
> 1.5e1_       // invalid: _ must separate successive digits
> ```

Quotes from _The Go Programming Language Specification, Version of June 29, 2022_

Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


Architecture-independent Numeric Types

Go provides several predeclared numeric types. Most of them have a fixed size, regardless of the architecture the program is compiled for or running on. Numeric Types An integer, floating-point, or complex type represents the set of integer, floating-point, or complex values, respectively. They are collectively called numeric types. The predeclared architecture-independent numeric types are: uint8 the set of all unsigned 8-bit integers (0 to 255) uint16 the set of all unsigned 16-bit integers (0 to 65535) uint32 the set of all unsigned 32-bit integers (0 to 4294967295) uint64 the set of all unsigned 64-bit integers (0 to 18446744073709551615) int8 the set of all signed 8-bit integers (-128 to 127) int16 the set of all signed 16-bit integers (-32768 to 32767) int32 the set of all signed 32-bit integers (-2147483648 to 2147483647) int64 the set of all signed 64-bit integers (-9223372036854775808 to 9223372036854775807) float32 the set of all IEEE-754 32-bit floating-point numbers float64 the set of all IEEE-754 64-bit floating-point numbers complex64 the set of all complex numbers with float32 real and imaginary parts complex128 the set of all complex numbers with float64 real and imaginary parts byte alias for uint8 rune alias for int32 The value of an n-bit integer is n bits wide and represented using two’s complement arithmetic.


Numeric type aliases

Numeric Types … byte alias for uint8 rune alias for int32 … To avoid portability issues all numeric types are defined types and thus distinct except byte, which is an alias for uint8, and rune, which is an alias for int32. As with other aliases in Go, this means that the same type simply has two (or perhaps more) identifiers, which are completely interchangeable. This means that byte is not a distinct type, simply backed by a uint8 type.


Architecture-specific Numeric Types

In addition to the architecture-independent numeric types, Go provides three architecture-specific numeric types. Although there are far fewer of them, they often get a lot more usage. Numeric Types … There is also a set of predeclared integer types with implementation-specific sizes: uint either 32 or 64 bits int same size as uint uintptr an unsigned integer large enough to store the uninterpreted bits of a pointer value What does it mean for a type’s size to be architecture-dependent?