I’ve seen many discussions about whether Go should add enum support to the language. I’m not going to bother arguing for or against but instead show how to make due with what we have in the language now.
A very short enum intro
Enumerated types, or enums, represent finite sets of named values. They are usually introduced to signal that variables can only take one of the predefined values for the enum. For example, we could have an enum called Colors
with members Red
, Green
, Blue
. Usually, the members are represented as integer values, starting from zero. In this case, Red
would correspond to 0, Green
to 1 and Blue
to 2 with Red
, Green
and Blue
being the names of corresponding members. They help simplify the code as they are self-documenting and explicitly list all possible values for the given type. In many languages enums will also return compile errors if you’ll try to assign to an invalid value. However, since enums do not exist in Go, we do not have such guarantees.
Defining a custom type
Usually, one of the first steps of defining an enum in Go is to define a custom type for it. There are 2 commonly used types for this purpose, string
and int
. Let’s start with strings:
type Color string
const (
Red Color = "red"
Green Color = "green"
Blue Color = "blue"
)
I actively avoid defining my enums in this style, since using strings increases the likelihood of errors. You don’t really know if the enum members are defined in uppercase, lowercase, title case or something else entirely. Besides, there is a high chance of miss-spelling the strings both in the definition and subsequent use and blue
becomes bleu
. I also often use bitmasks so that might influence my judgement, but I’ll talk about bitmasks in a separate post at some point.
For these reasons I prefer an int declaration:
type Color int
const (
Red Color = 0
Green Color = 1
Blue Color = 2
)
Keep in mind that this is highly subjective and people will have their own preferences. Also, the int definition does not read as nicely when displayed, but we will fix that later.
Iota in go
In the colors example, I’m only using three colors, but what if we had 5, 10 or 20 of them? It would be quite tedious to assign values to each and every single one of them. Luckily, we can simplify this by using the iota
keyword that Go provides:
type Color int
const (
Red Color = iota
Green
Blue
)
fmt.Println(Red, Green, Blue) // 0 1 2
Iota acts as syntactic sugar, automatically incrementing the value for each successive integer constant in a constant declaration.
If we’d like to start at another number, we can achieve this with the following:
type Color int
const (
Red Color = iota + 42
Green
Blue
)
fmt.Println(Red, Green, Blue) // 42 43 44
You can also use a variety of expressions on iota but I hardly recommend that except for the most trivial of cases as this leads to code that is hard to read and comprehend. One of the common use cases of such expressions which is still readable is defining bitmasks:
type Color int
const (
Red Color = 1 << iota
Green
Blue
_ // this skips one value
Yellow
)
fmt.Println(Red, Green, Blue, Yellow) // 1 2 4 16
For more on iota, please refer to the Go spec.
One thing to note is that you should be very careful when making changes to already established constant declarations with iota. It’s easy to cause headaches if you remove or change the order of members as these could have already been saved to a database or stored in some other way. Once you ingest those, what was once blue might become red so keep that in mind.
While such declarations might suffice as an enum in some circumstances you usually will expect more from your enum. For starters, you’d like to be able to return the name of the member. Right now, a fmt.Print(Red)
will print 0
, but how would we print the name? How would I determine if 4
is valid color or not? I’m also able to define a custom color by simply defining a variable var Brown Color = 7
. What if I’d like to marshal my enum to their string representation when returning this via an API? Let’s see how we can address some of these concerns.
Getting the member name
Since we’ve defined Color as a custom type we can implement the stringer interface on it to get the member names.
func (c Color) String() string {
switch c {
case 0:
return "Red"
case 1:
return "Green"
case 2:
return "Blue"
}
return fmt.Sprintf("Color(%q)", int(c))
}
We can now print the name by calling the .String()
method on any of the Color
and get the names out. There are many ways one could implement this method but all of them have the same caveat - whenever I add a new color in my constant declaration, I will also need to modify the .String()
method. Should I forget to do so, I’ll have a bug on my hands.
Luckily, we can leverage code generation with the stringer tool can help us. It can generate the code required for our Color enum to implement the stringer interface. You’ll need to have the stringer tool installed so run go install golang.org/x/tools/cmd/stringer@latest
to do that. Afterwards, include the following directive, I usually plop it right above my enum type declaration:
//go:generate stringer -type=Color
type Color int
If you run go generate ./...
you’ll see a colors_string.go
file appear, with the stringer interface implemented, allowing you to access the names of the members like so:
fmt.Println(Red.String(), Green.String(), Blue.String()) // Red Green Blue
Marshalling and unmarshalling
If we use our Color
enum in a struct and marshal it it will be represented as int:
type MyResponse struct {
Color Color
}
bts, _ := json.Marshal(MyResponse{Color: Blue})
fmt.Println(string(bts)) // {"Color":4}
Sometimes, this behavior might suit you. For instance, you might be OK if the value is stored as an integer however if you’re exposing this information to the end user it might make sense to display the color name instead. To achieve this, we can implement the MarshalText() ([]byte, error)
method for our Color
enum. I’m specifically implementing the MarshalText
over MarshalJSON
as the latter falls back to using MarshalText
internally in the std libs json library. This means that by implementing it we will get the color represented as string in the marshalled form for both JSON, XML and text representations and, depending on the implementation, perhaps other formats and libraries.
func (c Color) MarshalText() ([]byte, error) {
return []byte(c.String()), nil
}
// ...
bts, _ := json.Marshal(MyResponse{Color: Blue})
fmt.Println(string(bts)) // {"Color":"Blue"}
If we’d like to accept string colors as input, we’ll have to do a bit more work. First, we’ll need to be able to determine if a given string is a valid color for our enum or not. To achieve this, let’s implement a ParseColor
function
var ErrInvalidColor = errors.New("invalid color")
func ParseColor(in string) (Color, error) {
switch in {
case Red.String():
return Red, nil
case Green.String():
return Green, nil
case Blue.String():
return Blue, nil
}
return Red, fmt.Errorf("%q is not a valid color: %w", in, ErrInvalidColor)
}
Once again, we could implement this in many different ways, but they will have the downside that if we’re ever expanding our Color
enum, we’ll have to go into the ParseColor
and extend it to support our new members. There are tools that can generate this for us, and I’ll talk about them later.
With this, we can implement the UnmarshalText
method and unmarshal an input with colors as strings like so:
func (c *Color) UnmarshalText(text []byte) error {
parsed, err := ParseColor(string(text))
if err != nil {
return err
}
*c = parsed
return nil
}
type MyRequest struct {
Color Color
}
dest := MyRequest{}
_ = json.Unmarshal([]byte(`{"Color": "Blue"}`), &dest)
fmt.Println(dest.Color.String()) // Blue
If an invalid color is provided, the unmarshalling will result in an ErrInvalidColor
error.
Similarly, we could implement the Valuer
and Scanner
interfaces for database interactions:
func (c *Color) Scan(v interface{}) error {
col, ok := v.(string)
if !ok {
return fmt.Errorf("could not convert %T to color", v)
}
color, err := ParseColor(col)
if err != nil {
return err
}
*c = color
return nil
}
func (c Color) Value() (driver.Value, error) {
return c.String(), nil
}
Reducing boilerplate
If you’re working with quite a few enums and need the custom marshalling and stringer/valuer/scanner interface implementations it can become quite tedious having to do all these steps for each of your enums. Everything that I’ve discussed so far can be generated with the go-enum library. With it, the enum definition becomes a bit different:
//go:generate go-enum --marshal --sql
// ENUM(Red, Green, Blue)
type Color int
If you run go generate ./...
a file will be generated including all the custom marshalling, parsing and stringer/valuer/scanner implementations. This is a great tool if you work with multiple enums and have to do this often.
Another alternative that leverages generics and avoids generation is the enum library.
Both of these are valid options and it is up to the reader to choose one that suits your needs. I will go over my preferences at the end of this blog post.
Improving type safety by using structs
There’s one caveat with these enums and that’s the fact that one can just construct a new enum member by hand. There’s nothing preventing me from defining a var Yellow = Color(3)
and passing that to a function expecting a color:
func UseColors(c Color) {
// would actually do something useful
fmt.Println(c.String())
}
var Yellow = Color(3)
UseColors(Yellow) // completely valid code
Firstly, I would like to say that there is no bulletproof way to protect from this, but there are some things you can do.
If we define our enum as a struct in a separate package:
package colors
type Color struct {
id int
name string
}
var (
Red = Color{id: 0, name: "Red"}
Green = Color{id: 1, name: "Green"}
Blue = Color{id: 2, name: "Blue"}
)
You would obviously include ways to construct valid enums from outside the package by either the ID or name, the methods for serializing, stringifying and any other needs you have. However, this only provides an illusion of safety, since you can do any of the following:
- Reassign one color as another:
colors.Red = colors.Blue
. - Create an uninitialized color:
var myColor = colors.Color{}
.
For 2) we could shift our enum by 1, and include an unknown value with the id of 0.
var (
Unknown = Color{id: 0, name: ""}
Red = Color{id: 1, name: "Red"}
Green = Color{id: 2, name: "Green"}
Blue = Color{id: 3, name: "Blue"}
)
We’d still have to handle this unknown value in any code dealing with colors:
func UseColors(c colors.Color) error {
if c == colors.Unknown {
return errors.New("received unknown color")
}
// do something with the valid colors
return nil
}
Since structs can not be declared const, we have to become inventive to cover ourselves from 1). We can define the colors as funcs:
func Unknown() Color {
return Color{}
}
func Red() Color {
return Color{id: 1, name: "Red"}
}
func Green() Color {
return Color{id: 2, name: "Green"}
}
func Blue() Color {
return Color{id: 3, name: "Blue"}
}
With this in place, you can no longer assign a color as another.
An alternative approach would be to make the color type an unexported int and only export its members:
type color int
const (
Red color = iota
Green
Blue
)
To make this type even remotely useful, we could export and implement a Colors interface:
type Color interface {
ID() int
Name() string
Equals(Color) bool
}
func (c color) ID() int {
return int(c)
}
func (c color) Name() string {
switch c {
case 0:
return "Red"
case 1:
return "Green"
case 2:
return "Blue"
}
return fmt.Sprintf("Color(%q)", int(c))
}
func (c color) Equals(in Color) bool {
return c.ID() == in.ID()
}
You could then use it like this:
func UseColors(c colors.Color) {
if c.Equals(colors.Red) {
// do something with red
}
}
In theory, you could still write a custom implementation of this interface and create a custom color like that but I think that is highly unlikely to happen.
However, I’m not a big fan of these approaches as they seem a tad cumbersome while providing little in return.
My personal enum preferences
With all this said, I’d like to point out a few things that have been working quite well for me in practice and my general experience:
- Enums are not as widespread as one might think. Even in large code bases, you’re very likely to have only a handful of enums. Using libraries for this might be overkill.
- I value consistency very highly when it comes to code so I usually follow whatever pattern is already established for enum definitions in the existing code base.
- I only ever reference existing enum members and try to never type cast to an enum.
- For fresh projects, I use an int based type with iota with any additional interfaces implemented by hand. If the time comes where this becomes tedious to maintain, I switch to code generation.
- Unless enums become part of the language spec, I’ll stick to using iota based enums. Any fancy tricks to add more type safety to them just add more boilerplate. I trust the people I work with, our review processes and don’t feel the need for these extra safety measures.
This is just my opinion and it might not match the situation you’re in. Always choose what works best for you!
If you have any suggestions or alternatives, I’d be glad to hear them in the comments below.
Top comments (0)