Stricter Go enums with go-genums

gdm85
Where do we Go now
Published in
3 min readSep 18, 2015

Go has an idiomatic way to declare enums, that is by many (me included) praised for its simplicity:

type Day intconst (
Monday Day = 1 + iota
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
)

Other languages have usually more features for enum types, for example in Java:

public enum Day {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
}

And then you can use features like:

for (Day day: Day.values()) {
if (day == Day.Monday) {
System.out.printf("Monday is day number %d\n", day.ordinal());
}
System.out.printf("Day of the week: %s\n", day);
}

In C# you can benefit from similar type-safety checks, and a slightly different syntax for the base type:

enum Days : byte {Monday=1, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};

Let’s give now another look at the same enum in Go, with focus on what we could make use of:

type Day int // a String() method for each value, so that we can printf it during development/logging?const (
Monday Day = 1 + iota // more complex types maybe?
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
// boundary check so that I cannot inadvertently create an 8th day of the week?
)
// array/slice of allowed values?

In a discussion with other Go coders I came up with the idea of using a slightly stricter syntax for Go enums, (ab)using type switches and the marvels of templating (with go:generate), the result of which is the go-genums project.

The generated code for this weekdays example with go-genums is (some parts redacted for simplicity):

package main

// *** generated with go-genums ***

// DayEnum is the the enum interface that can be used
type DayEnum interface {
String() string
Value() dayEnum
uniqueDayMethod()
}

// dayEnumBase is the internal, non-exported type
type dayEnumBase struct{ value dayEnum }

// Value() returns the enum value
func (eb dayEnumBase) Value() dayEnum { return eb.value }

// String() returns the enum name as you use it in Go code,
// needs to be overriden by inheriting types
func (eb dayEnumBase) String() string { return "" }

// ... [redacted declaration of a type+methods for each allowed enum value]

var internalDayEnumValues = []DayEnum{
Sunday{}.New(),
Monday{}.New(),
Tuesday{}.New(),
Wednesday{}.New(),
Thursday{}.New(),
Friday{}.New(),
Saturday{}.New(),
}

// DayEnumValues will return a slice of all allowed enum value types
func DayEnumValues() []DayEnum { return internalDayEnumValues[:] }

// NewDayFromValue will generate a valid enum from a value, or return nil in case of invalid value
func NewDayFromValue(v dayEnum) (result DayEnum) {
switch v {
case daySunday:
result = Sunday{}.New()
case dayMonday:
result = Monday{}.New()
case dayTuesday:
result = Tuesday{}.New()
case dayWednesday:
result = Wednesday{}.New()
case dayThursday:
result = Thursday{}.New()
case dayFriday:
result = Friday{}.New()
case daySaturday:
result = Saturday{}.New()
}
return
}

// ... [redacted]

The resulting generated enum code is nonetheless longer and more complex, but you do not have to type it as it’s auto-generated; among the features added by this code generation step, the one I personally like most is the possibility to use type switches for enum evaluation:

day := Monday{}.New()switch day.(type) {
case Monday:
fmt.Println("It's", day)
default:
panic("It's not Monday!")
}

Note here that a type switch over an interface value is still pretty efficient, as it will not de-reference the actual value (be it an int or a large struct) but rather use Go’s internal type id.

Last but not least, with go-genums you can use structs for your enum declaration and auto-generated validating factory methods, and you do not loose the capability of directly comparing enum variables; you can read more and play around with it at the github project page.

--

--

gdm85
Where do we Go now

Thinker, software developer, cryptography passionate and an avid reader of science and technology.