Back to Notes

Go Enums

Overview

Go has no built-in enum keyword. The idiomatic alternative is a typed constant group combined with iota — a compile-time counter that auto-increments within a const block. The result behaves like an enum: a named type with a fixed set of named values.


Basic iota Pattern

iota starts at 0 and increments by 1 for each constant in the block.

type Direction int

const (
    North Direction = iota // 0
    East                   // 1
    South                  // 2
    West                   // 3
)

func main() {
    d := North
    fmt.Println(d) // 0  ← not ideal for printing
}

Adding a String() Method — The Stringer Interface

Raw integer output isn't readable. Implement the fmt.Stringer interface (String() string) so your enum prints its name instead of its number.

type Direction int

const (
    North Direction = iota
    East
    South
    West
)

func (d Direction) String() string {
    return [...]string{"North", "East", "South", "West"}[d]
}

fmt.Println(North)  // North
fmt.Println(South)  // South

d := East
fmt.Printf("Heading: %v\n", d) // Heading: East

go generate tip: The stringer tool (golang.org/x/tools/cmd/stringer) auto-generates String() methods — avoids keeping the array in sync manually.


Skipping Values with iota

Use _ to skip the zero value (common when 0 should mean "unset" or "unknown"):

type Status int

const (
    _       Status = iota // 0 — skip; treat as unset
    Pending               // 1
    Active                // 2
    Closed                // 3
)

Or start from a specific value:

type Priority int

const (
    Low    Priority = iota + 1 // 1
    Medium                     // 2
    High                       // 3
)

Bitmask / Flags with iota

Use 1 << iota to create powers of two — ideal for flags that can be combined with bitwise OR.

type Permission uint

const (
    Read    Permission = 1 << iota // 1  (001)
    Write                          // 2  (010)
    Execute                        // 4  (100)
)

func (p Permission) String() string {
    var parts []string
    if p&Read != 0    { parts = append(parts, "Read") }
    if p&Write != 0   { parts = append(parts, "Write") }
    if p&Execute != 0 { parts = append(parts, "Execute") }
    return strings.Join(parts, "|")
}

perm := Read | Write
fmt.Println(perm)          // Read|Write
fmt.Println(perm&Execute)  // 0 — Execute not set

Enums Across Multiple const Blocks

iota resets to 0 at the start of each const block. Group related constants together to avoid accidental overlap.

type Color int
const (
    Red Color = iota // 0
    Green            // 1
    Blue             // 2
)

type Size int
const (
    Small Size = iota // 0 — iota resets
    Medium            // 1
    Large             // 2
)

Exhaustive switch on Enums

Go's compiler does not warn about missing enum cases in a switch. Convention: add a default panic or use a linter (exhaustive) to catch unhandled cases.

func describe(d Direction) string {
    switch d {
    case North:
        return "going north"
    case East:
        return "going east"
    case South:
        return "going south"
    case West:
        return "going west"
    default:
        panic(fmt.Sprintf("unknown direction: %v", d))
    }
}

Validating Enum Values

Since the underlying type is int, any integer can be cast to your enum type. Validate at boundaries (user input, deserialization):

func ParseDirection(s string) (Direction, error) {
    switch s {
    case "North": return North, nil
    case "East":  return East, nil
    case "South": return South, nil
    case "West":  return West, nil
    default:
        return 0, fmt.Errorf("unknown direction: %q", s)
    }
}

Gotchas

GotchaDetail
Zero value is a valid enum valueIf iota starts at 0, the zero value of your type is a named constant — this can cause bugs when using zero as "not set". Skip it with _
No exhaustiveness checksThe compiler won't error on a switch missing cases — use the exhaustive linter
Invalid castsAny int can be cast to your enum type — always validate at system boundaries
iota resets per const blockTwo separate blocks can produce constants with the same underlying values — don't mix them
Printing without String()Without a Stringer implementation, fmt.Println prints the raw integer — always add String() for types that appear in logs or output