Floating-Point Literals

A complete guide to Go's floating-point literals—decimal and hexadecimal forms, exponent notation, digit separators, compile-time evaluation, and precision traps.

Go provides two syntactic forms for writing floating-point constants: decimal floating-point literals and hexadecimal floating-point literals. Both forms allow you to include an exponent and to use underscores to group digits for readability. Before a literal is assigned to a typed variable, it exists as an untyped, arbitrarily precise constant at compile time. That distinction—between the high-precision constant and the runtime IEEE 754 value—explains much of the behavior people find surprising.

Decimal Floating-Point Form

A decimal floating-point literal represents a number using familiar base‑10 notation, optionally scaled by a power of ten. The syntax is built from four parts, three of which are optional in specific combinations:

  • an integer part (decimal digits)
  • a decimal point
  • a fractional part (decimal digits)
  • an exponent part (e or E, followed by an optional + or sign and decimal digits)

You can leave out the integer part or the fractional part, but not both — the literal must contain at least one digit. You can leave out the decimal point or the exponent, but again the literal must have at least one of them. If both the decimal point and the exponent are missing, the literal becomes an integer literal, not a floating‑point one.

One of each pair must stay:

You must keep at least one digit side (integer part or fractional part) and at least one scaling indicator (decimal point or exponent). The combinations 6. and 6e0 are both legal; e0 alone is not.

The exponent means “multiply the mantissa by 10 raised to the given power.” For example, 1.5e2 is 1.5 × 10² = 150, and 9e‑3 is 9 × 10⁻³ = 0.009.

// Valid decimal floating-point literals
3.14        // classic form
.5          // integer part omitted
72.40       // trailing zero in fraction
1.e+0       // fraction omitted, positive exponent sign
6.022e23    // Avogadro's number
1E6         // 1 million, uppercase E
0.15e+0_2   // 0.15 × 10² = 15, with digit separator (see below)

Each of these is a compile‑time constant with exact decimal value. The compiler evaluates them using extended precision before eventually rounding to a concrete float32 or float64 when they are bound to a typed variable.

How to think about the exponent

Treat the e (or E) as a shorthand for “× 10^”. 3e4 is read “3 times 10 to the fourth”. This is the same convention used in scientific calculators and many other programming languages, so the muscle memory transfers directly.

A common beginner slip is writing an integer literal like 10 with an exponent but omitting the decimal part, e.g. 10e2. That works — it is a floating‑point literal, not an integer. If you need the value to stay an integer, you must use integer arithmetic or an explicit conversion.

All valid decimal forms compile without error:

Every combination that follows the spec’s optional rules is accepted by the Go compiler. If your literal compiles, you are free to use it in any expression where a floating‑point constant is allowed.

Hexadecimal Floating-Point Form

Hexadecimal floating-point literals let you express a number using base‑16 digits and a binary exponent. This form is invaluable when you need precise control over the bit pattern of a floating-point value — for example, when defining constants that match IEEE 754‑specified boundaries.

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)
  • a mandatory exponent part (p or P, followed by an optional sign and decimal digits)

You may omit the integer part or the fractional part, and you may even omit the radix point, but the exponent part is required. The exponent uses decimal digits but scales the mantissa by 2 raised to that power — not 10. So 0x1p‑2 means (1 hex) × 2⁻² = 0.25.

// Valid hexadecimal floating-point literals
0x1p-2          // 1 × 2⁻² = 0.25
0x2.p10         // 2.0 × 2¹⁰ = 2048.0
0x1.Fp+0        // (1 + 15/16) × 2⁰ = 1.9375
0X.8p-0         // 0.5 × 2⁰ = 0.5
0X_1FFFP-16     // 8191 × 2⁻¹⁶ ≈ 0.12498
0x1.921fb54442d18p+1  // π approximation in hex

Notice that the exponent p or P is followed by decimal digits, even though the mantissa is hexadecimal. That mixing of bases is intentional and matches the IEEE 754‑2008 standard, which Go follows. The exponent expresses the power of two in a human‑readable decimal form.

Forgetting the p exponent or using e will break:

A literal like 0x1.5 is illegal because a hexadecimal floating-point literal must contain a p/P exponent. Similarly, 0x1.5e‑2 is not a hex float — the e is a hexadecimal digit (14) followed by an operator and an integer. The compiler will treat it as subtraction, likely producing a type error.

When to use the hexadecimal form

Most of the time, decimal floating-point literals are the right tool. You reach for the hexadecimal form in three specific situations:

  • defining exact IEEE 754 constants such as math.MaxFloat32 internally
  • expressing floating‑point numbers that are derived from bit‑exact algorithms (e.g., math library implementations)
  • specifying a value that must round in a precise, documented way at compile time

If none of those describe your code, a decimal literal will be clearer to the next reader.

Digit Separators in Floating-Point Literals

Long digit sequences are hard to scan. Go allows an underscore _ as a visual separator between digits in both decimal and hexadecimal floating-point literals. The underscore has no effect on the value — it is purely cosmetic.

1_000_000.5     // one million point five
0x1_AAAA_5555p0 // easier to count nibbles
3.14159_26535   // 10 decimal digits of π
1.5e+1_2        // 1.5 × 10¹² = 1.5 trillion

Rules for placement:

  • You cannot place an underscore at the beginning or end of the literal (_1.5 and 1.5_ are invalid).
  • An underscore must separate successive digits — 1__5 is not allowed.
  • After a base prefix (0x or 0X) you may place an underscore immediately before the first digit: 0x_1p0 is legal.
  • You may place an underscore between a digit and the radix point, and between a digit and the exponent letter, as long as the underscore does not become the first or last character. So 1_5. is legal, 1._5 is legal, and 1.5_e1 is legal — each underscore sits between digits.

Underscores near the exponent or decimal point can confuse readers:

While 1.5_e1 and 1_.5 are technically valid, they look odd. Use separators only inside digit runs — for example 1_234.567_89 — to keep the code easy to skim.

Compile-Time Evaluation and Constant Precision

A floating-point literal is an untyped constant. The Go compiler evaluates it with extended precision using a representation that supports at least a 256‑bit mantissa and a 16‑bit exponent, as required by the language specification. This means the literal 0.1 is known to the compiler exactly as the rational number 1/10, not as a truncated float64 approximation.

Only when the constant is assigned to a variable, passed as a function argument, or used in a context that forces a specific floating-point type does the compiler round it to the nearest representable value of that type (for float32, 32 bits; for float64, 64 bits). If the value is too large to be represented even after rounding, the compiler stops with an “overflows” error.

package main
import "fmt"
func main() {
    const c = 0.1          // untyped, exact
    var f float64 = c      // rounded to nearest float64
    var g float32 = c      // rounded to nearest float32
    fmt.Printf("float64: %.30f\n", f)
    fmt.Printf("float32: %.30f\n", g)
}

Running this code will show that f and g differ slightly because the two types have different precision, even though they both came from the same exact constant 0.1.

This compile-time precision is the reason why constant expressions can produce results that differ from the same computation performed on variables at runtime. At compile time, 10.1 * 3.0 is evaluated as an exact rational (101/10 times 3) and then rounded to the final type, while at runtime, a float64 variable holding the rounded approximation of 10.1 already carries a small error before the multiplication happens.

x := 10.1                     // x is float64 (rounded approx)
fmt.Println(x == 10.1)        // true: constant folded to same float64 as x
fmt.Println(x*3.0 == 10.1*3.0) // false: left side uses runtime mul, right side constant folded

In the first comparison, both sides become the same float64 constant, so they compare equal. In the second, the right side is evaluated at compile time with full precision, while the left side multiplies the already‑approximated x at runtime. The difference in rounding paths leads to different results.

Avoid direct equality on computed floats:

Because of rounding differences between compile‑time and runtime evaluation, comparing two floating‑point values with == is fragile. Use a small tolerance (math.Abs(a-b) < 1e‑9) when you need to decide if two computed values are “the same.”

Common Mistakes and Misconceptions

Confusing hexadecimal float and integer subtraction

A sequence like 0x15e-2 is not a hexadecimal floating-point literal. It is the integer literal 0x15e (350 in decimal) followed by a subtraction operator and the integer 2. Since hexadecimal floating‑point literals require a p exponent, any hex string without a p is either an integer or an error.

Using a p exponent with a decimal mantissa

1p-2 is not a valid floating-point literal because the mantissa is decimal and the exponent is binary. The compiler will reject it. You can write 0x1p-2 or 1e-2, but not a mix.

Believing that 0.1 is stored exactly in a float64

The constant 0.1 is exact at compile time, but once stored in a float64 it becomes the closest 64‑bit IEEE 754 approximation, which is slightly larger than 0.1. Printing with high precision reveals the difference.

Misplacing digit separators

Beginners sometimes write 1_.5 or _1.5. The compiler rejects these because an underscore cannot be the first or last character of a literal. Stick to placing underscores between digits inside long runs.

Forgetting the default type

An untyped floating-point literal has a default type of float64. When you write x := 6.022e23, the variable x becomes a float64. If you need a float32, declare the type explicitly: var x float32 = 6.022e23 or use a typed constant.

You control the final type:

Although the literal starts as untyped, you can bind it to any floating‑point type by specifying the type in the declaration or assignment. The compiler handles the rounding for you.

Summary

Floating-point literals in Go give you two notation systems: a familiar decimal form with a power‑of‑ten exponent, and a hexadecimal form with a power‑of‑two exponent for exact bit‑level control. Both forms support digit separators, making large constants readable without changing their value. The compiler holds these literals as high‑precision constants until they are forced into a concrete type, which gives constant expressions their exactness but also creates a subtle behavioral difference compared to the same math performed on variables.

Understanding this constant‑versus‑variable boundary is what protects you from surprising comparison results and helps you choose the right literal form for the job.