To Enum or Not To Enum

Enum-like values have come up in my reviews of other people’s code a few times, and I’d like to nail down what we feel is best practice.

I’ve seen many places what in other languages would be an enum, i.e. a bounded list of known values that encompass every value that should ever exist.

The code I have been critical of simply calls these values strings, and creates a few well-known values, thusly: package tool

// types of tools
const (
    ScrewdriverType = "screwdriver"
    HammerType = "hammer"
   // ...
)

type Tool struct {
    typ string
}

func NewTool(tooltype string) (Tool, error) {
    switch tooltype{
        case ScrewdriverType, HammerType:
            return Tool{typ:tooltype}, nil
        default:
            return Tool{}, errors.New("invalid type")
    }
}

The problem with this is that there’s nothing stopping you from doing something totally wrong like this:

name := user.Name()

// ... some other stuff

a := NewTool(name)

That would fail only at runtime, which kind of defeats the purpose of having a compiler.

I’m not sure why we don’t at least define the tool type as a named type of string, i.e.

package tool

type ToolType string

const (
    Screwdriver ToolType = "screwdriver"
    Hammer = "hammer"
   // ...
)

type Tool struct {
    typ ToolType
}

func NewTool(tooltype ToolType) Tool {
        return Tool{typ:tooltype}
}

Note that now we can drop the error checking in NewTool because the compiler does it for us. The ToolType still works in all ways like a string, so it’s trivial to convert for printing, serialization, etc.

However, this still lets you do something which is wrong but might not always look wrong: a := NewTool("drill") Because of how Go constants work, this will get converted to a ToolType, even though it’s not one of the ones we have defined.

The final revision, which is the one I’d propose, removes even this possibility, by not using a string at all (it also uses a lot less memory and creates less garbage):

package tool

type ToolType int

const (
    Screwdriver ToolType = iota
    Hammer
   // ...
)

type Tool struct {
    typ ToolType
}

func NewTool(tooltype ToolType) Tool {
        return Tool{typ:tooltype}
}

This now prevents passing in a constant string that looks like it might be right. You can pass in a constant number, but NewTool(5) is a hell of a lot more obviously wrong than NewTool("drill"), IMO.

The push back I’ve heard about this is that then you have to manually write the String() function to make human-readable strings… but there are code generators that already do this for you in extremely optimized ways (see https://github.com/golang/tools/blob/master/cmd/stringer/stringer.go)

w