Working with Constants in Golang
Published Updated Golang 12 min read
Untyped constants have arbitrary precision and no type until they are used, which is why 1e100 compiles and why a typed constant can be less useful than an untyped one. Plus iota and the enum pattern.
Go refuses to mix numeric types. And yet this compiles:
const big = 1 << 62
var f float64 = big
var i int64 = big
One constant assigned to two different types, in a language that will not let you assign an int to
an int64. The explanation is that big has no type at all until it is used, and untyped constants
are the most useful part of Go’s constant system.
Written against Go 1.22.
Constants are compile-time only
const pi = 3.14159
const greeting = "hello"
const debug = false
const start = time.Now() // compile error: not a constant expression
const buf = []byte("x") // compile error: slices are not constant
A constant must be computable at compile time, so only booleans, runes, integers, floats, complex numbers and strings qualify. No slices, maps, structs, function calls or addresses. That restriction is what makes the rest possible.
Untyped constants have arbitrary precision
An untyped constant is a value, not a value-of-a-type. The spec requires the compiler to represent integer constants to at least 512 bits, so intermediate results can exceed every machine type:
const huge = 1 << 200 // fine — no type, no overflow
const ratio = huge / (1 << 190) // 1024, computed exactly
fmt.Println(ratio) // 1024
var x int = huge // compile error: constant overflows int
The error arrives at the point of use, not declaration. huge itself is legal; asking for it as
an int is not. This is also why math.MaxUint64 can appear in an expression that no runtime type
could hold, as long as the final result fits.
Float constants are exact too:
const third = 1.0 / 3.0 // exact within the compiler's precision
var f32 float32 = third // rounded to float32 here
var f64 float64 = third // rounded to float64 here — more accurate
The rounding happens once, at the assignment, from the exact value. Computing in float32 throughout
would compound error at every step.
Default types
When an untyped constant is used where no type is implied. It takes its default type:
x := 42 // int
y := 3.14 // float64
z := 'A' // rune (int32)
s := "hi" // string
b := true // bool
c := 1 + 2i // complex128
That is the only reason x := 42 produces an int rather than an int8. It is a default, not an
inference from the value.
Typed constants are more restrictive
const untypedOne = 1
const typedOne int = 1
var f float64
f = untypedOne // fine — the constant adopts float64
f = typedOne // compile error: cannot use typedOne (int) as float64
Giving a constant a type opts it out of the flexibility. That is occasionally what you want. A
constant that must be a time.Duration and nothing else — and usually not:
// less useful
const timeout int = 30
// more useful: works wherever a numeric type is expected
const timeout = 30
// most useful when the unit is part of the meaning
const timeout = 30 * time.Second
That last one is a typed constant, because time.Second is typed, and here the type carries
meaning, so restricting it is the point. time.Sleep(30) sleeps 30 nanoseconds; time.Sleep(timeout)
does what it says.
Constant expressions
Arithmetic on untyped constants is exact and happens at compile time:
const (
KB = 1 << 10
MB = 1 << 20
GB = 1 << 30
)
const maxUpload = 10 * MB // 10485760, computed at compile time
Mixing a typed and an untyped constant produces the typed one’s type; mixing two different typed constants is an error:
const a int32 = 1
const b int64 = 2
const c = a + b // compile error: mismatched types int32 and int64
Division follows the operands. Two integer constants divide as integers:
const half = 1 / 2 // 0 — integer division
const halfF = 1.0 / 2 // 0.5 — one float operand makes it float
The same rule as runtime arithmetic, applied at compile time, and the same surprise.
iota, and enums
iota is the index of the current ConstSpec in a const block, starting at zero:
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
)
The type and expression repeat implicitly on each subsequent line, which is why only the first needs
writing. iota resets to 0 in every new const block.
Skip a value with _:
const (
_ = iota // skip 0
KB = 1 << (10 * iota) // 1 << 10
MB // 1 << 20
GB // 1 << 30
TB // 1 << 40
)
That is the idiomatic byte-size block, and it works because the expression 1 << (10 * iota) is
repeated with an incrementing iota.
Bit flags:
type Permission uint8
const (
Read Permission = 1 << iota // 1
Write // 2
Execute // 4
)
p := Read | Write
fmt.Println(p&Write != 0) // true
Two things to know about enum-like constants in Go.
They are not a closed set. Weekday(99) is a valid Weekday. The type gives you a name and
method attachment, not exhaustiveness. There is no compiler check that a switch covers every case.
Validate at the boundary:
func (d Weekday) Valid() bool { return d >= Sunday && d <= Saturday }
The zero value is the first constant, so var d Weekday is Sunday. If “unset” needs to be
distinguishable, spend the zero on it:
const (
UnknownWeekday Weekday = iota
Sunday
Monday
)
For readable output, add a String() method, or generate one with stringer, which keeps it in step
with the constants:
func (d Weekday) String() string {
return [...]string{"Sunday", "Monday", "Tuesday"}[d]
}
Note that this panics on an out-of-range value, which is another reason to validate.
Where constants genuinely help
Beyond naming magic numbers, two cases:
Compile-time array sizes. An array length must be a constant:
const bufferSize = 4096
var buf [bufferSize]byte // fine
Exact values with no runtime cost. A constant expression is folded by the compiler, so
10 * MB is a literal in the binary with no arithmetic at runtime.
What constants cannot be is anything computed: a value read from configuration, a time.Now(), a
slice literal. Those are variables, and if they must not change, the convention is an unexported
variable with an exported accessor.
Frequently asked questions
What is an untyped constant?
A constant with no type until it is used. It adopts the type required
at the point of use, which is why one untyped constant can be assigned to a float64 and an int64.
Why does 1 << 200 compile?
Untyped integer constants have at least 512 bits of precision. The overflow error appears only when you assign it to a type that cannot hold it.
When should I give a constant an explicit type?
When the type carries meaning and other types
would be wrong, a time.Duration, or your own enum type. Otherwise leave it untyped for flexibility.
What is the default type of 42?
int. Of 3.14, float64; 'A', rune; 1+2i, complex128.
That default applies only where no other type is implied.
Why is 1 / 2 zero?
Both operands are integer constants, so it is integer division: the same rule
as at runtime, applied at compile time. Write 1.0 / 2.
Can a constant be a slice or struct?
No. Only booleans, runes, integers, floats, complex numbers and strings can be constant. Use a variable for anything else.
How does iota work?
It is the index of the current specification within a const block, starting
at 0, and it resets in each new block. Omitted expressions repeat the previous line’s.
Are Go enums type-safe?
Only partially. A named integer type gives names and methods but not a
closed set: Weekday(99) is valid, and no compiler check requires a switch to be exhaustive.
What is the zero value of an enum type?
Whatever constant equals 0, normally the first. If “unspecified” must be distinguishable, define an explicit unknown value at 0.
Do constant expressions cost anything at runtime?
No. They are folded at compile time, so
10 * MB appears in the binary as a literal.
Where should I go next?
Basic types and conversion covers the runtime side of the same rules, and packages covers where these constants live.