HowtoGo
Home / Basics / Constants
Basics

Constants

Go supports constants of character, string, boolean, and numeric values.

const declares a constant value.

const s string = "constant"

A const statement can appear anywhere a var statement can.

const n = 500000000

Constant expressions perform arithmetic with arbitrary precision.

const d = 3e20 / n
fmt.Println(d)

A numeric constant has no type until it's given one, such as by an explicit conversion.

fmt.Println(int64(d))

Division on untyped constants keeps full precision until a type is forced.

Terminal
$ go run constants.go
6e+11
600000000000

Constants can't hold runtime values

const works for the environment variable's name, a string known at compile time. The key itself isn't: os.Getenv reads it from the process environment at runtime, a function call, so it has to be a var.

const apiKeyEnv = "API_KEY"

apiKey := os.Getenv(apiKeyEnv)
if apiKey == "" {
    log.Fatal("API_KEY not set")
}

The key never appears in the program's source or its output, only its length. Logging a real secret, even for debugging, is how it ends up somewhere it shouldn't.

Terminal
$ API_KEY=sk-demo-12345 go run constants.go
loaded a 13-character key

iota: Go's enumerated constants

Go has no dedicated enum keyword. iota is the idiomatic replacement: inside a const block it starts at 0 and increments by one for every line, so Sunday through Saturday get consecutive values without writing them out.

type Weekday int

const (
    Sunday Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

Weekday is just int underneath. Printing a value straight from the const block prints that underlying number, not a name.

fmt.Println(Wednesday)
fmt.Println(int(Wednesday))

Wednesday is the fourth name in the block, so its iota value is 3. int(Wednesday) makes the conversion explicit; both lines print the same number either way.

Terminal
3
3

A leading _ = iota discards the 0 value without using it. iota keeps counting on skipped lines too, so KB starts from 1, and the same expression, evaluated with iota at 1, 2, and 3, produces each size a thousand times the last.

const (
    _  = iota // skip 0
    KB = 1 << (10 * iota)
    MB
    GB
)

fmt.Println(KB, MB, GB)

1024, 1024², and 1024³, the standard binary byte-size ladder, generated from one expression instead of three separate literals.

Terminal
1024 1048576 1073741824

1 << iota is the other common pattern: each constant becomes the next power of two, so every value occupies its own bit. Combining them with | builds a set of flags; checking one with & tests whether that bit is set.

type Permission int

const (
    Read Permission = 1 << iota
    Write
    Execute
)

perms := Read | Execute
fmt.Println(perms&Write != 0)
fmt.Println(perms&Read != 0)

perms only has Read and Execute set. Masking with &Write comes back zero; masking with &Read doesn't, since that bit is actually set.

Terminal
false
true

A type implementing String() string satisfies fmt.Stringer, and every fmt print function checks for it automatically. Attaching one to Weekday turns the same underlying integer into readable output everywhere it gets printed, no call site changes needed.

func (d Weekday) String() string {
    names := [...]string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
    return names[d]
}

fmt.Println(Wednesday) // fmt calls String() automatically

Same value, same fmt.Println(Wednesday) call as before, now printing Wednesday instead of 3, purely because the type grew a String method.

Terminal
Wednesday