Type Safe Enums in Go
I’m willing to bet that many Go programmers have seen or used this strategy for enumerating things in code before:
type Profession int
const (
	Unknown Profession = iota
	Warrior
	Cleric
	Hunter
	Mage
)In practice, this is probably fine. In fact, I can’t think of a time this has caused a (known) bug in one of my programs (yet.) Famous last words.
However, there are times where I wish the solution was a little more bulletproof at compile-time.
For example, I dislike having an Unknown variant. This is mainly to avoid ambiguity when client code introduces a zero-value for this enum and perhaps forgets to set it, like so:
var prof Profession
_ = whatIsYourProfession(prof)This means that most code that cares about the enum type will have to account for the Unknown variant, like so:
func whatIsYourProfession(p Profession) error {
	switch p {
	case Unknown:
		return errors.New("unknown profession")
	case Warrior:
		fmt.Println("Couldn't beat you in an arm-wrestling match.")
	case Cleric:
		fmt.Println("Divine!")
	case Hunter:
		fmt.Println("Shoot an arrow over them mountains.")
	case Mage:
		fmt.Println("I'll pick a card, any card.")
	}
	return nil
}Even though I accounted for all of the professions, there’s still a bug here.
Technically, client code could do this:
if err := whatIsYourProfession(Profession(-1)); err == nil {
	fmt.Println("fooled ya!")
}Again, in practice, it’s unlikely that client code would pull the pin on that grenade and hand it to us, but I wish the compiler would stop them from doing so.
So I guess we could add a default case:
func whatIsYourProfession(p Profession) error {
	switch p {
	case Unknown:
		return errors.New("unknown profession")
	case Warrior:
		fmt.Println("Couldn't beat you in an arm-wrestling match.")
	case Cleric:
		fmt.Println("Divine!")
	case Hunter:
		fmt.Println("Shoot an arrow over them mountains.")
	case Mage:
		fmt.Println("I'll pick a card, any card.")
	default:
		return errors.New("how could you betray me like this?")
	}
	return nil
}We could prevent the zero-value situation by sealing the type within our package so client code can’t zero-value construct it without using specific constructor functions:
package profession
import (
	"errors"
)
var (
	ErrUnknown error = errors.New("unknown profession")
)
type Profession func() profession
func (p Profession) String() string {
	var s string
	switch {
	case Is(p, Warrior):
		s = "warrior"
	case Is(p, Cleric):
		s = "cleric"
	case Is(p, Hunter):
		s = "hunter"
	case Is(p, Mage):
		s = "mage"
	}
	return s
}
type profession int
const (
	warrior profession = iota
	cleric
	hunter
	mage
)
func FromString(s string) (Profession, error) {
	switch s {
	case "warrior":
		return Warrior, nil
	case "cleric":
		return Cleric, nil
	case "hunter":
		return Hunter, nil
	case "mage":
		return Mage, nil
	}
	return nil, ErrUnknown
}
func Warrior() profession {
	return warrior
}
func Cleric() profession {
	return cleric
}
func Hunter() profession {
	return hunter
}
func Mage() profession {
	return mage
}
func Is(r, target Profession) bool {
	return r() == target()
}Now there’s no such thing as an unknown zero-value profession. Furthermore, because the underlying type is sealed in the package and only accessible via constructor functions, the worst thing client code can do is pass a nil function pointer through if they create a zero-value (nil pointer) profession.Profession:
var prof profession.Profession // nilWhether or not that offends you, is up to you. Representing an invalid state is a bug, and I’d rather segfault than limp along and hope for the best despite garbage state flowing through my program.
Let’s tie it all together with a complete example program, where we can now have peace of mind that garbage enum values won’t be able to quietly sneak through.
package main
import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"strings"
	"codeofconnor.com/profession/profession"
)
func main() {
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		prof, err := profession.FromString(line)
		if errors.Is(err, profession.ErrUnknown) {
			fmt.Println("I've never heard of a", line)
			continue
		}
		whatIsYourProfession(prof)
	}
}
func whatIsYourProfession(prof profession.Profession) {
	var message string
	switch {
	case profession.Is(prof, profession.Warrior):
		message = "Couldn't beat you in an arm-wrestling match."
	case profession.Is(prof, profession.Cleric):
		message = "Divine!"
	case profession.Is(prof, profession.Hunter):
		message = "Shoot an arrow over them mountains."
	case profession.Is(prof, profession.Mage):
		message = "I'll pick a card, any card."
	default:
		message = "I've never met a " + prof.String() + " before."
	}
	fmt.Println(message)
}% ./p
warrior
Couldn't beat you in an arm-wrestling match.
hunter
Shoot an arrow over them mountains.
mage
I'll pick a card, any card.
cleric
Divine!
potato
I've never heard of a potato