Go has had first-class compile-time constants since 1.0 (March 2012), and they behave unlike anything in JavaScript or Python. A Go const has no memory address — the compiler inlines it everywhere it appears. It must be a compile-time-evaluable expression. And it can be untyped, which means it adopts whichever numeric type the surrounding code requires — 3.14 can stand in for float32, float64, or your own type Currency float64 without a cast. Add iota, the constant generator, and you have everything you need for clean, type-safe enumerations and bitmasks. This guide covers the const rules, iota patterns, the standard library constants worth memorizing, and the serialization gotcha that bites most Go teams once.
What can a Go const hold?
A constant in Go is a name bound to a value the compiler can evaluate at build time. Allowed kinds: booleans, runes, strings, integers, floating-point, and complex numbers. No structs, no slices, no maps, no function calls (except a small whitelist like len on a string literal).
const MaxRetries = 3
const DefaultTimeout = 30 * time.Second
const APIBaseURL = "https://api.example.com/v1"
// Grouped — the idiomatic style for related constants
const (
StatusOK = 200
StatusNotFound = 404
StatusError = 500
)What's the difference between typed and untyped constants?
An untyped constant has a "default kind" — integer, float, complex, rune, or string — but adopts the concrete type of whatever context uses it. This is one of the cleanest pieces of Go's type system: you can declare const Pi = 3.14159 and use it as a float32 in one place and float64 in another with no conversion. A typed constant pins the type at the declaration and rejects implicit conversion.
// Untyped — adopts whichever float type the use site needs
const Pi = 3.14159265358979323846
// Typed — pinned to float64
const TypedPi float64 = 3.14159265358979323846
var f32 float32 = Pi // OK — Pi adapts to float32
// var f32 float32 = TypedPi
// → cannot use TypedPi (untyped float constant 3.141592...) as float32 valueUntyped numeric constants also have higher precision than the language's largest float type — the spec guarantees at least 256 bits of mantissa during compile-time arithmetic, so multiplications and divisions between constants do not lose precision until they meet a typed value.
How does iota actually work?
iota is a predeclared identifier that resets to 0 at every const block and increments by one with each line of constant specification. The name is borrowed from APL. Combine it with shifts, multiplication, and the blank identifier, and you can build enumerations, bitmasks, and unit scales:
// Simple enumeration
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
// Bitmask flags — left-shift by iota
type Permission uint
const (
Read Permission = 1 << iota // 1
Write // 2
Execute // 4
)
// Byte-size units — skip 0 with the blank identifier
const (
_ = iota // skip 0
KB = 1 << (10 * iota) // 1024
MB // 1048576
GB // 1073741824
TB // 1099511627776
)The _ = iota trick is worth its own callout: it makes the zero value of your type unused, so you can detect "uninitialized" at runtime. Without the skip, var d Direction would silently equal North, which is rarely what you want.
How do you build a type-safe enum in Go?
Go does not have a dedicated enum keyword. The idiom is a named integer type plus a const block of typed values, optionally with a String() method for readable output. The stringer tool from golang.org/x/tools auto-generates that method via go generate, so you do not have to maintain the name array by hand.
//go:generate stringer -type=Direction
type Direction int
const (
North Direction = iota
East
South
West
)
func move(d Direction) {
fmt.Println("Moving", d) // "Moving North"
}
move(North) // OK
// move(42) // cannot use untyped int 42 as Direction in argument to moveWhich built-in constants ship with Go?
The language predeclares only three: true, false, and iota. Everything else lives in standard library packages — most usefully math:
import "math"
math.Pi // 3.141592653589793
math.E // 2.718281828459045
math.Phi // 1.618033988749895 (golden ratio)
math.SqrtE // 1.6487212707001282
math.MaxFloat64 // ~1.8e308
math.MaxInt64 // 9223372036854775807
math.MinInt64 // -9223372036854775808
// Integer overflow constants per type
math.MaxInt32 // 2147483647
math.MinInt32 // -2147483648Practical patterns and gotchas
- iota is line-based: blank lines count as zero increments, comments do not. If you reorder lines, your numeric values change. Add a doc-comment block above the const block explaining what the values are committed to.
- Inserting a value mid-list breaks serialized data: if you have
Pending = iota; Active; Completedand you stash these as integers in a database, slippingCancelledin between renumbers everything fromCancelledonward. Append, never insert. - Capitalization controls visibility:
MaxRetriesis exported,maxRetriesis package-private. Same rule as functions and types — there is no separateprivatekeyword. - No const for slices, maps, or structs. The compiler will reject them. Use a package-level
varwith// ReadOnly: do not mutateas a doc-comment, or expose a getter that returns a copy.
For Go's environment variable story, including envconfig and godotenv, see environment variables in Go. For language-agnostic naming and immutability rules, see constants best practices.