Go Constants: const and iota Explained

Constants are immutable values that are evaluated at compile time. Unlike variables, once you declare a constant, its value cannot be changed during program execution. This immutability provides...

Key Insights

  • Constants in Go are immutable values evaluated at compile time, providing type safety and performance benefits over variables for values that never change.
  • The iota identifier creates auto-incrementing enumerations within const blocks, resetting to 0 at each new const declaration and incrementing for each line.
  • Use bit-shifting patterns with iota (like 1 << iota) to create efficient flag systems, and explicit values when the actual constant value matters for external APIs or protocols.

Introduction to Constants in Go

Constants are immutable values that are evaluated at compile time. Unlike variables, once you declare a constant, its value cannot be changed during program execution. This immutability provides several advantages: the compiler can optimize constant usage more aggressively, you get stronger type safety guarantees, and your code’s intent becomes clearer when readers know a value will never change.

Use constants when you have values that remain fixed throughout your program’s lifetime: mathematical constants, configuration defaults, enum-style values, or any identifier that represents a fixed concept in your domain.

Here’s the fundamental difference between constants and variables:

package main

import "fmt"

func main() {
    const maxRetries = 3
    var currentRetries = 0
    
    // This works fine
    currentRetries = 1
    currentRetries = 2
    
    // This won't compile: cannot assign to maxRetries
    // maxRetries = 5
    
    fmt.Println(maxRetries, currentRetries)
}

The compiler enforces immutability at compile time, catching bugs before your code ever runs. This is particularly valuable in large codebases where accidental reassignment could introduce subtle bugs.

Declaring Constants: Syntax and Types

Go provides flexible syntax for declaring constants. You can declare them individually or group them in blocks, similar to variable declarations.

Single constant declarations are straightforward:

const Pi = 3.14159
const AppName = "MyApplication"
const MaxConnections = 100

For related constants, grouped declarations improve readability:

const (
    StatusPending   = "pending"
    StatusActive    = "active"
    StatusCompleted = "completed"
    StatusFailed    = "failed"
)

Go’s constant system distinguishes between typed and untyped constants. Untyped constants have greater flexibility because they maintain higher precision and can be used in more contexts:

const (
    // Untyped constants
    UntypedInt = 42
    UntypedFloat = 3.14
    
    // Typed constants
    TypedInt int = 42
    TypedFloat float64 = 3.14
)

func main() {
    // Untyped constant can be used as different types
    var i32 int32 = UntypedInt
    var i64 int64 = UntypedInt
    
    // Typed constant requires explicit conversion
    var i32_2 int32 = int32(TypedInt)
    
    // Untyped constants maintain precision
    const Precise = 1.0 / 3.0
    fmt.Printf("%.50f\n", Precise) // High precision maintained
}

Untyped constants are generally preferred unless you need to satisfy a specific interface or ensure a particular type constraint.

The iota Identifier: Auto-Incrementing Constants

The iota identifier is Go’s mechanism for creating auto-incrementing constant enumerations. Within each const block, iota starts at 0 and increments by 1 for each constant specification line.

Basic enumeration with iota:

const (
    Sunday = iota    // 0
    Monday           // 1
    Tuesday          // 2
    Wednesday        // 3
    Thursday         // 4
    Friday           // 5
    Saturday         // 6
)

When you omit the expression for subsequent constants in a block, Go repeats the previous expression with an incremented iota value. This makes enumerations concise.

You can use the blank identifier to skip values:

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

Multiple constants can reference the same iota value:

const (
    A, B = iota, iota + 10  // 0, 10
    C, D                     // 1, 11
    E, F                     // 2, 12
)

Remember that iota resets to 0 in each new const block:

const (
    First = iota  // 0
    Second        // 1
)

const (
    Third = iota  // 0 (reset)
    Fourth        // 1
)

Advanced iota Patterns

The real power of iota emerges when you combine it with expressions. Bit-shifting with iota creates efficient flag systems:

type Permission uint

const (
    PermissionRead Permission = 1 << iota // 1 (binary: 001)
    PermissionWrite                        // 2 (binary: 010)
    PermissionExecute                      // 4 (binary: 100)
)

func (p Permission) HasPermission(check Permission) bool {
    return p&check != 0
}

func main() {
    // Combine permissions with bitwise OR
    userPerms := PermissionRead | PermissionWrite
    
    fmt.Println(userPerms.HasPermission(PermissionRead))    // true
    fmt.Println(userPerms.HasPermission(PermissionExecute)) // false
}

This bit-flag pattern is memory-efficient and allows combining multiple flags in a single value.

For unit conversions, multiply iota by scaling factors:

type ByteSize int64

const (
    _  = iota // ignore first value by assigning to blank identifier
    KB ByteSize = 1 << (10 * iota)
    MB
    GB
    TB
)

func (b ByteSize) String() string {
    switch {
    case b >= TB:
        return fmt.Sprintf("%.2fTB", float64(b)/float64(TB))
    case b >= GB:
        return fmt.Sprintf("%.2fGB", float64(b)/float64(GB))
    case b >= MB:
        return fmt.Sprintf("%.2fMB", float64(b)/float64(MB))
    case b >= KB:
        return fmt.Sprintf("%.2fKB", float64(b)/float64(KB))
    }
    return fmt.Sprintf("%dB", b)
}

You can use complex mathematical expressions:

const (
    MaxSize = 100
    
    Small  = MaxSize / 10      // 10
    Medium = MaxSize / 2       // 50
    Large  = MaxSize           // 100
    
    // Combining iota with expressions
    Priority1 = (iota + 1) * 100  // 100
    Priority2                      // 200
    Priority3                      // 300
)

Best Practices and Common Pitfalls

Follow Go’s naming conventions: use MixedCaps for exported constants and mixedCaps for unexported ones. For enum-style constants, prefix them with a common identifier:

// Good: Clear enum-style naming
const (
    OrderStatusPending   = "pending"
    OrderStatusConfirmed = "confirmed"
    OrderStatusShipped   = "shipped"
    OrderStatusDelivered = "delivered"
)

// Avoid: Generic names without context
const (
    Pending = "pending"
    Confirmed = "confirmed"
)

Use explicit values when the actual constant value matters for external systems, protocols, or storage:

// Good: HTTP status codes must match the standard
const (
    StatusOK                   = 200
    StatusCreated              = 201
    StatusBadRequest           = 400
    StatusUnauthorized         = 401
    StatusNotFound             = 404
    StatusInternalServerError  = 500
)

// Bad: Using iota for values that have external meaning
const (
    StatusOK = iota  // 0 - wrong!
    StatusCreated    // 1 - wrong!
    StatusBadRequest // 2 - wrong!
)

Reserve iota for internal enumerations where the specific numeric value doesn’t matter:

// Good: Internal state machine
const (
    stateIdle = iota
    stateConnecting
    stateConnected
    stateDisconnecting
)

Be aware that iota is scoped to its const block and resets in each new block. Adding constants at the beginning of an iota enumeration changes all subsequent values:

const (
    TypeA = iota // 0
    TypeB        // 1
    TypeC        // 2
)

// Later, someone adds TypeD at the beginning
const (
    TypeD = iota // 0 - now TypeA's old value!
    TypeA        // 1 - changed!
    TypeB        // 2 - changed!
    TypeC        // 3 - changed!
)

If insertion order stability matters, start your iota at 1 and reserve 0 for an “undefined” or “invalid” sentinel value.

Real-World Use Cases

Here’s a complete example showing a connection state machine with proper constant usage:

package main

import (
    "fmt"
    "time"
)

// ConnectionState represents the state of a network connection
type ConnectionState int

const (
    StateDisconnected ConnectionState = iota
    StateConnecting
    StateConnected
    StateReconnecting
    StateError
)

func (s ConnectionState) String() string {
    return [...]string{
        "Disconnected",
        "Connecting",
        "Connected",
        "Reconnecting",
        "Error",
    }[s]
}

// ConnectionFlags uses bit flags for connection options
type ConnectionFlags uint

const (
    FlagTLS ConnectionFlags = 1 << iota
    FlagCompression
    FlagKeepAlive
    FlagAutoReconnect
)

type Connection struct {
    state ConnectionState
    flags ConnectionFlags
}

func (c *Connection) HasFlag(flag ConnectionFlags) bool {
    return c.flags&flag != 0
}

func (c *Connection) Connect(flags ConnectionFlags) {
    c.flags = flags
    c.state = StateConnecting
    
    fmt.Printf("Connecting with flags: TLS=%v, Compression=%v, KeepAlive=%v\n",
        c.HasFlag(FlagTLS),
        c.HasFlag(FlagCompression),
        c.HasFlag(FlagKeepAlive))
    
    // Simulate connection
    time.Sleep(100 * time.Millisecond)
    c.state = StateConnected
}

func main() {
    conn := &Connection{}
    
    // Connect with TLS and auto-reconnect
    conn.Connect(FlagTLS | FlagAutoReconnect)
    
    fmt.Printf("Connection state: %s\n", conn.state)
    fmt.Printf("Has TLS: %v\n", conn.HasFlag(FlagTLS))
    fmt.Printf("Has Compression: %v\n", conn.HasFlag(FlagCompression))
}

This example demonstrates proper constant usage: iota for internal state enumeration where values don’t matter externally, bit flags for efficient option storage, and a custom String method for debugging. The pattern is type-safe, memory-efficient, and clearly expresses intent.

Constants and iota are fundamental Go features that, when used correctly, make your code more maintainable and efficient. Use explicit values for external contracts, iota for internal enumerations, and bit-shifting patterns for flags. Follow naming conventions, and your constants will serve as clear, immutable anchors throughout your codebase.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.