env.dev

Constants in Go: const, iota & Type-Safe Enumerations

Go const is compile-time only — no memory address, inlined at every use. Untyped constants adopt their use-site type. iota generates enum sequences and bitmasks.

Last updated:

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).

go
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.

go
// 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 value

Untyped 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:

go
// 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
//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 move

Which 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:

go
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 // -2147483648

Practical 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; Completed and you stash these as integers in a database, slipping Cancelled in between renumbers everything from Cancelled onward. Append, never insert.
  • Capitalization controls visibility: MaxRetries is exported, maxRetries is package-private. Same rule as functions and types — there is no separate private keyword.
  • No const for slices, maps, or structs. The compiler will reject them. Use a package-level var with // ReadOnly: do not mutate as 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.

Was this helpful?

Read next

Constants in Python: typing.Final, Enum & SCREAMING_SNAKE

Python has no const keyword — constants are SCREAMING_SNAKE_CASE by PEP 8 convention. typing.Final (Python 3.8) adds static-checker enforcement; the enum module handles closed sets.

Continue →

Frequently Asked Questions

What can a Go const hold?

Booleans, runes, strings, integers, floats, complex numbers, and expressions composed of constants. Not structs, slices, maps, or function calls (with a small whitelist like len on a string literal). The compiler must be able to evaluate the value at build time.

What is the difference between typed and untyped constants in Go?

An untyped constant has a "kind" (integer, float, string) but adopts the concrete type the use site requires. const Pi = 3.14159 can stand in for float32, float64, or a user-defined float type with no cast. A typed constant (const TypedPi float64 = 3.14) is pinned and rejects implicit conversion.

How does iota actually work?

iota resets to 0 at every const block and increments by 1 with each line of constant specification — not each name, each line. Combined with shifts and the blank identifier _, you can build enumerations, bitmasks (1 << iota), and unit scales (1 << (10 * iota) for KB/MB/GB).

Why should I skip the zero value with _ = iota?

Because the zero value of an integer type is 0 by default. Without skipping, var d Direction silently equals the first variant. Skip 0 with _ = iota so the zero value signals "uninitialized" instead of accidentally matching North.

Stay up to date

Get notified about new guides, tools, and cheatsheets.