3 tips for (slightly) better Go iota enums

Go has no native enum types; the most idiomatic way of representing an enumerator is to use constants, many times along with iota.

Here are three tips that can make for (just slightly) better Go iota enums.

Add one more ‘count’ element

I first saw this trick in the standard library, eg. when enumerating Goroutine states, register bounds or when marking GC roots.

In the most usual case it allows to build simpler validation methods.

type method uint8

const (
    Get method = iota
    Post
    Patch
    ...
    methodCount
)
func (m method) IsValid() bool {
    return m < methodCount
}

In other cases it may allow optimizations like using fixed-size arrays instead of slices.

Don’t use […]string{}; try a map instead

I’ve seen people use [...]string{ when implementing the Stringer interface for their enum type.

Let’s say you’re building a Fighting game in Go, and you want to represent character states.

type State int

const (
    Standing State = iota
    Walking
    Crouched
)

func (s State) String() string {
    return [...]string{"Stand", "Walk", "Crouch"}[s]
}

What’s wrong with this snippet? First off, it will happily accept fmt.Println(State(-1)) and fmt.Println(State(100)) and panic. It’s also a little harder to maintain since you need to keep the element order in mind.

Well how about

var stateNames = map[State]string{
    Standing: "Stand",
    Walking:  "Walk",
    Crouched: "Crouch",
}

func (s State) String() string {
    // just return stateNames[s], or
    v, ok := stateNames[s]
    if !ok {
        // do stuff
        return ""
    }
    return v
}

The map also provides constant access time, instead of linear in the case of the string array.

Try un-exporting enum types

Also, in the spirit of ‘making invalid states unrepresentable’, think about un-exporting the enumerator type. The types themselves can be exported, as well as their methods, but this will disallow people from instantiating their own State(100).

Bonus - Use uint8 instead of int

Why not use a uint8 instead of an int? The ‘iota’ works the same, you get simpler validation, plus a small performance improvement, almost for free.

Written on October 24, 2020