How I (usually) do enums in Go

When something can be modelled as “one of these mutually exclusive options”, I usually reach for an interface with a marker method in Go:

type SomeEnum interface {
	isSomeEnum()
}

type AnEnumValue struct{}
func (AnEnumValue) isSomeEnum() {}

type AnotherEnumValue struct{}
func (AnotherEnumValue) isSomeEnum() {}

type EnumValueWithPayload struct{
	A string
	B int
}

func (EnumValueWithPayload) isSomeEnum() {}

This feels almost kinda roughly like algebaraic types (at least sum types), except that nil is always a valid interface value. This means that you’ll still need to check for validity, but you get things like “An enum of value XYZ implies that there’s a list of strings somewhere” for free:

func (e SomeEnum) error {
	switch e := e.(type) {
	case EnumValueWithPayload:
		// Freely use e.A and e.B here
	case AnotherEnumValue:
		// No need to check whether A and B are valid, they don't exist
	}
}

This is a rough approximation of “make invalid states unrepresentable”. Some edge values (such as nil) do still creep in and need to be accounted for, but a lot of the “A and B only exists if the enum is EnumValueWithPayload” type shenanigans don’t need to be explicitly checked for.