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.