Skip to content
CalliCoder

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.