DEV Community

Viktoras
Viktoras

Posted on • Originally published at dizzy.zone on

Enums in Go

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"
)
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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))
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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}

Enter fullscreen mode Exit fullscreen mode

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"}

Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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"}
)
Enter fullscreen mode Exit fullscreen mode

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:

  1. Reassign one color as another: colors.Red = colors.Blue.
  2. 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"}
)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"}
}
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

You could then use it like this:

func UseColors(c colors.Color) {
    if c.Equals(colors.Red) {
        // do something with red
    }
}
Enter fullscreen mode Exit fullscreen mode

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)