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 generatetip: Thestringertool (golang.org/x/tools/cmd/stringer) auto-generatesString()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
| Gotcha | Detail |
|---|---|
| Zero value is a valid enum value | If 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 checks | The compiler won't error on a switch missing cases — use the exhaustive linter |
| Invalid casts | Any int can be cast to your enum type — always validate at system boundaries |
iota resets per const block | Two 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 |