Some Go enum tricks
Hey. This is the first article of this blog. I go over some simple things to warmup my writing skills.
Go is known to be a simple language. It is part of its philosophy, which focuses on productivity and maintainability. But this can sometimes be a problem. I recently came across one of those situations when I needed to use enums. Go does not support enums, but we can approximate them in different ways. The simplest way is to use constants:
|
|
iota
is a special keyword which starts at 0 and automatically increments the next constant who belongs to the same const
scope.
This implementation lacks several properties commonly found in enums. Let’s look at some techniques we can use to improve this.
Here, the enums type default to int
. We can define a type alias to int
and use this new type to define methods on our enums.
This does not provide us with type safety however, meaning that we can pass any int
value to a function accepting a type Planet
,
even if this value is not mapped to a valid planet. Since the default value of an int
is 0
, we discard this value so that we
can differentiate between specific values provided on purpose from
the default value of an int
which might indicate an error. But how do we make sure that a variable of this type holds
a correct planet? We create a method on the Planet
type along with a dummy last value _planetNumber
that is not exported
to automatically check for the correct size. This is useful when we often add or remove constants.
|
|
Nice. Having the enum to hold an int
allows us to easily verify if it is valid. Now we might want to know more about it,
like its name. Go has a builtin interface called Stringer
. Any type that has a method String() string
implements
this interface. You could manually implement this interface. But if you have a large amount of enums, or are constantly
modifying them, manually maintaining a slice of those string representations can be gruesome.
Automated code generation
Like any respectable computer languages, Go is Turing complete. Therefore, it can generate its own code!
Using the //go generate some_tool.bin
, we can call any binary at compile time. One of those particularly useful binary
is stringer: it makes our type automatically implement the Stringer
interface
by having the name of the constant being its string representation. First run in your terminal:
$ go get golang.org/x/tools/cmd/stringer
In the code, we add a coomented line which is recognized by the go generate
command:
|
|
Finally, we call go generate
in the directory of the package containing the type. To execute all //go:generate
statements in your module, you can use:
$ go generate ./...
This will create a file planet_string.go
with the following content:
|
|
This is incredibly useful, and I have been using this throughout the Go packages I have been writing in my work. There are many other
tools which you can use with go generate
, like [ifacemaker]{https://github.com/vburenin/ifacemaker} to generate interfaces from structs
and mockery to generate mocks from interfaces.
Reversing the relationship
Now that we can get a string representation of a constant automatically, what about the reverse?
If you look at the file planet_string.go
, you can see the array _Planet_index
and the string _Planet_name
.
Those two variables can be used to reverse the relationship and obtain the corresponding Planet
type from its string representation !
|
|
If your constants contain more than 10 “runs” (consecutive integer values), the content of this file will be very different.
This is because stringer
will use a map _Planet_map
instead of the list _Planet_index
to balance between space and complexity.
In that case, we can reassign this map to a public variable and use it directly to reverse the relationship!
|
|
That’s all folks.
Nota bene: the Sun and Pluto are no planets.