Constant Expressions
How Go evaluates constant expressions at compile time with arbitrary precision and determines their resulting type
A constant expression is any expression in Go where every operand is itself a constant — a literal, a named constant, or a sub‑expression built entirely from constants. Because the compiler knows the exact value of every part of the expression, it can compute the result once, before the program ever runs. That single fact unlocks two powerful behaviours: compile‑time evaluation and arbitrary‑precision arithmetic.
Understanding constant expressions matters because they sit at the boundary between Go’s flexible, untyped constant world and its strict, typed variable world. They let you write 1<<70 or 3/2.0 without casts, yet still catch overflow mistakes before you ship.
Compile-Time Evaluation
When the Go compiler sees a constant expression, it does not emit code that calculates the result at runtime. Instead, it evaluates the expression immediately, using internal logic that is separate from the normal CPU arithmetic used for variables. The outcome is a new constant value that behaves exactly as if you had written that value as a literal.
const secondsPerDay = 60 * 60 * 24
The right‑hand side is three integer literals multiplied together. The compiler multiplies them to get 86400, and secondsPerDay becomes the constant 86400. There is no runtime multiplication. Any expression that uses secondsPerDay later — say, secondsPerDay * 7 — will itself be simplified at compile time, and so on.
A constant expression stops being constant the moment you introduce a non‑constant operand.
func hoursToSeconds(h int) int {
return h * 60 * 60
}
Here h is a variable, so the whole multiplication happens at runtime. Even though 60 * 60 alone is a constant sub‑expression, the presence of h forces runtime evaluation. You can still help the compiler by extracting the constant part:
const secsPerHour = 60 * 60
func hoursToSeconds(h int) int {
return h * secsPerHour
}
Now secsPerHour is a pre‑computed constant, and the function does one runtime multiplication instead of two. For small numbers it makes no measurable difference, but the principle matters when constants carry the extra safety of compile‑time checks.
What qualifies as a constant operand:
A constant operand is any of these: a literal (42, "hello", true), a named constant declared with const, the predeclared identifier iota, the result of the built‑in functions len, cap, complex, real, or imag when their arguments are constants, or another constant expression. Variables, function calls (except the ones just listed), and value‑receiving operations like channel reads are never constant operands.
Arbitrary-Precision Arithmetic
Normal Go integer variables have fixed sizes — int8, int64, and so on — and operations on them wrap around or overflow silently at the bit width. Constant arithmetic does not. The compiler represents constant integers as mathematical integers with effectively unlimited precision. The same goes for floating‑point and complex constants: they are stored with much higher precision than float64 or complex128, only being rounded when they must be assigned to a typed variable.
This design frees you from worrying about intermediate overflow in a constant expression.
const (
Big = 1 << 100
Precise = 0.1 + 0.2 - 0.3
)
Big is a 101‑bit integer. That value cannot fit in any built‑in integer type, but as an untyped constant it exists perfectly. You can even multiply it further inside another constant expression without loss. Precise is 5.551115123125783e-17, not the 0 you would get from float64 arithmetic. The compiler stores constants with a 512‑bit (or larger) mantissa and avoids the usual floating‑point errors until you explicitly force a type.
Arbitrary precision in practice:
If you can express a value as a constant expression, the compiler will hold it with full accuracy. Only when you assign it to a typed variable or pass it to a typed function parameter does the value get squeezed into a fixed‑size representation. That is the moment where overflow or precision loss is reported — at compile time, not at 3 a.m. in production.
This precision also means you can write constants for things like maximum unsigned integers without resorting to runtime tricks:
const MaxUint = ^uint(0) // all bits set, evaluated at compile time
const MaxInt = int(MaxUint >> 1)
^uint(0) is a constant expression; the compiler computes the bitwise complement of zero using the full mathematical value, then the shift works on that huge integer. No overflow happens inside the constant expression. Only when you use MaxUint in a context that demands uint does the value get truncated to 64 (or 32) bits — which, for ^uint(0), is exactly the right result.
How the Result Type Is Determined
Constant expressions often mix different “kinds” of numbers — integers with floats, floats with complex numbers. Since the operands themselves are untyped, the compiler follows a set of rules to decide what kind of constant the whole expression becomes.
Binary operations with untyped constants
When two untyped constants are combined with a binary operator (like +, -, *, /), the result stays untyped. Its kind is the one that appears later in this list: integer, rune, floating‑point, complex. An integer plus a float yields a float constant. A float divided by a complex number yields a complex constant.
const a = 2 + 3.0 // 5.0, untyped floating-point
const b = 15 / 4 // 3, untyped integer (integer division)
const c = 15 / 4.0 // 3.75, untyped floating-point
const d = 1 - 0.5i // untyped complex (float to complex promotion)
For comparison operators (==, !=, <, <=, >, >=), the result is always an untyped boolean constant.
const cmp = "hello" > "goodbye" // true, untyped boolean
These rules mean you can write a constant expression that naturally produces the “biggest” kind involved, without a single explicit conversion.
Shift operations
Shift expressions work differently. A shift expression x << y or x >> y has a left operand x and a right operand y. The right operand must always be an integer (or untyped integer). The type of the result depends entirely on the left operand:
- If
xis an untyped constant, the result is an untyped integer constant. - If
xis a typed constant of integer type, the result has that same type.
const shift1 = 1 << 3.0 // 8, untyped integer (left operand untyped)
const shift2 = 1.0 << 3 // 8, untyped integer (still untyped, result kind integer)
A typed left operand restricts the result type early, which can trigger overflow checks:
const shift3 = int32(1) << 33 // compile error: constant 8589934592 overflows int32
Here int32(1) makes the left operand a typed int32 constant. The shift tries to produce 8589934592 as an int32, which doesn’t fit, so the compiler rejects it. Without the int32 conversion, 1 << 33 would simply be a valid, huge untyped integer constant.
Shift overflow with typed left operand:
If you give the left operand a concrete integer type, the result must fit in that type’s range. The compiler treats any overflow as an error, even if you never assign the constant to a variable. Use untyped operands for intermediate constants that may be large, and only apply a type at the final assignment.
When typed constants are part of the expression
If any operand in a constant expression already has a concrete Go type, the expression’s result takes on that type. This is where untyped constants start behaving like normal typed values, and the usual type‑compatibility rules apply.
const typedFloat float64 = 3 / 2 // 1.0 — integer division happens first, then conversion to float64
const typedPi float64 = 3 / 2. // 1.5 — float division because 2. is a float constant, then stored in float64
In typedFloat, 3 / 2 is a constant expression with untyped integers, so it produces untyped integer 1. Only then is that 1 converted to float64 to satisfy the declared type, giving 1.0. Beginners often expect 1.5 and are surprised by the integer division. Making one operand a float (2. instead of 2) changes the arithmetic to floating‑point before the type conversion.
A typed constant also forces its type onto the rest of the expression, just like a typed variable would, but the evaluation still happens at compile time.
const (
a uint8 = 250
b uint8 = a + 10 // compile error: constant 260 overflows uint8
)
Both a and 10 are constants. The addition is performed at compile time, but because a is typed uint8, the result must fit in uint8. 260 doesn’t, so the compiler stops. If you had written const c = a + 10 without a type on c, c would be a typed uint8 constant as well — the type propagates from a. Removing the type from a would make the expression fully untyped and harmless.
Overflow and Precision Loss
Overflow in constant expressions is never silent. The compiler will refuse to compile code that stores a constant value into a variable or typed constant that cannot hold it.
var small int8 = 127
// var overflow int8 = 128 // error: constant 128 overflows int8
The moment you try to assign a constant to a typed location, the compiler checks whether the value fits. This is the same check that prevents int32(1) << 33 from compiling, and it works for floats and complex numbers too.
// const hugeFloat float32 = 3.4e40 // error: constant 3.4e+40 overflows float32
Precision loss behaves differently. When an untyped floating‑point or complex constant is assigned to a typed variable, the compiler rounds the value to the target type’s precision. This rounding is not an error, but the compiler will report it if the constant cannot be represented at all (overflow). Loss of a few low‑order bits is allowed and matches what would happen at runtime.
const precise = 0.1 + 0.2 - 0.3 // ~5.551115e-17 (untyped, full precision)
var x float64 = precise // x is 5.551115123125783e-17, exactly representable in float64
Integer division looks like rounding:
A constant expression 3/2 is integer division and yields 1, not 1.5. This surprises developers coming from languages where / always gives a float. If you want a floating‑point result, make one operand a float: 3/2. or 3.0/2. The mistake is most visible when a typed constant like float64(3/2) gives 1.0.
Common Mistakes and Misconceptions
Assuming all constant expressions produce floating‑point. The result kind follows the operands. Two untyped integers produce an integer; introduce one float to get a float. This is why const half = 1 / 2 sets half to 0, while const halfFloat = 1.0 / 2 gives 0.5.
Forgetting that typed constants constrain the expression early. A typed constant const a int32 = 1 << 30 is fine, but const b int32 = 1 << 31 is an overflow error, even though 1 << 31 alone is a valid untyped constant. The type propagates inward.
Using shifts with a non‑integer right operand. The right operand of << and >> must be an unsigned integer or an untyped constant representable as a uint. 1 << 2.5 is illegal, even though both are constants, because 2.5 is not an integer.
Thinking const makes a variable runtime‑constant. Go’s constants are purely compile‑time. You cannot use const to hold something that depends on runtime input. Trying to store the result of a function call (except len/cap/complex/real/imag on constant arguments) will fail.
Believing you can take the address of a constant. A constant does not live in memory at a fixed address; it’s just a value the compiler substitutes. &secondsPerDay will not compile. If you need an address, assign the constant to a variable first.
Practical Usage
Constant expressions are not just theoretical — they show up every day in realistic Go code. Mathematical constants often involve expressions:
const (
Pi = 3.14159265358979323846
TwoPi = 2 * Pi
PiOver2 = Pi / 2
)
Bitmask and flag constants are usually defined with shift expressions:
const (
FlagRead = 1 << iota // 1
FlagWrite // 2
FlagExec // 4
)
The combination of iota and constant expressions lets you build entire enumerated sets that the compiler computes once.
Capacity limits and buffer sizes become self‑documenting when derived from simple arithmetic:
const (
_ = iota
KB = 1 << (10 * iota) // 1024
MB // 1048576
GB // 1073741824
)
Because the values are constants, they carry the advantage of arbitrary precision; you can shift out to exabytes without worrying about integer overflow in the expression.
Even validation logic can be pushed to compile time using constant expressions and the fact that the compiler will reject illegal constant assignments:
const bitsPerWord = 32 << (^uint(0) >> 63) // 32 or 64, depending on platform
type word [bitsPerWord]byte // safe at compile time
Here the constant expression for bitsPerWord exploits the architecture‑dependent uint size, but the whole thing is resolved before execution.
Constants protect you from yourself:
Whenever you find yourself writing a magic number that must not change — a timeout, a maximum retry count, a cryptographic key size — write it as a constant expression. The compiler becomes your safety net, catching accidental modifications and arithmetic mistakes before they ever become a runtime bug.
Constant expressions are the mechanism that makes Go’s constants feel lightweight yet powerful. They explain why you can write math.Sqrt(2) without a cast, why you can define MaxUint64 without a function, and why integer overflows in configuration values are caught the moment you hit save. The key insight is that the compiler evaluates everything up front using unbounded precision, and only when the value must fit into a variable or typed constant does it enforce the usual size limits. This means your constants can be as large and as precise as the problem demands, while your variables stay safely within their defined types.