🎉 Announcing new lower pricing — up to 40% lower costs for Cloud Servers and Cloud SQL! Read more →

Generating Go Enumerations (Enums) That Work

As part of the upgrade to version 2 of the Brightbox Go API I’ve been using some of the newer features Go has introduced over the past few years: generators, generics and error wrapping.

Today we’ll be looking at Go enumerations, why they are useful, what you have to do to map them in and out of JSON, and how to use Go’s code generation capabilities to create them easily when required.

First, the problem

Version 1 of the Brightbox Go API is a very thin wrapper over a HTTP API call that decodes the returned JSON objects into structures using strings and integers. The Brightbox API has lots of enumerations for various attributes, with the main one being a status field which tells you what state an API object is in.

With version 1 this is a string comparison

if server.Status == "deleting" {
    return false, nil
}

Which is all fine until you have a senior moment and forget how to spell

if server.Status == "deleteing" {
    return false, nil
}

The compiler can’t help you here and you have an annoying bug that is difficult to track down.

Of course more than one type of object can be deleting, and since the status fields are all strings, it’s easy to get them mixed up

currentStatus := serverGroup.Status
// Sometime later...

if server.Status == currentStatus {
    return false, nil
}

Once again the compiler can’t help you out here, and it gets even worse when you introduce generic functions that are expected to take, and infer, the types of their arguments. It becomes very easy to mix things up in the code.

Really we want the compiler to complain when we spell things incorrectly or test a server group against a server. How can we get the Go type system to help us out?

Enter Go Enumerations

Go doesn’t have Enumeration types as such. Instead it fakes them via defined integer types and typed constants using auto generated integers.

package proxyprotocol

import (
    "encoding/json"
    "fmt"
    "reflect"
)

// Enum is an enumerated type
type Enum uint8

const (
    // V1 is an enumeration for proxyprotocol.Enum
    V1 Enum = iota + 1
    // V2 is an enumeration for proxyprotocol.Enum
    V2
    // V2Ssl is an enumeration for proxyprotocol.Enum
    V2Ssl
    // V2SslCn is an enumeration for proxyprotocol.Enum
    V2SslCn
)

Here we see a few of the tricks used to make a good enumeration in Go.

  • It’s stored in a separate package so the type reference proxyprotocol.Enum reads properly in any code that uses it
  • The package’s Enum type uses the smallest unsigned integer type necessary to hold the enumerations and is declared as a defined type, creating a new separate type, rather than an alias
  • The constants use iota to automatically generate unique incrementing integer values
  • This enumeration has no default value, so the first enumeration is set to start at 1 not 0

Now the compiler can catch spelling errors

$ go test
# github.com/terraform-providers/terraform-provider-brightbox/brightbox
brightbox/resource_brightbox_server.go:439:36: undefined: serverstatus.Deleteing

and incorrect comparisons

$ go test
# github.com/terraform-providers/terraform-provider-brightbox/brightbox
brightbox/resource_brightbox_server.go:440:23: invalid operation: obj.Status == loadbalancerstatus.Deleting (mismatched types serverstatus.Enum and loadbalancerstatus.Enum)
brightbox/resource_brightbox_server.go:441:17: invalid operation: obj.Status == loadbalancerstatus.Failed (mismatched types serverstatus.Enum and loadbalancerstatus.Enum)

However we can’t yet represent the enumeration as a string

./example_test.go:13:21: status.String undefined (type serverstatus.Enum has no field or method String)

for that we need to implement the Stringer interface.

Adding code generation

Go has a handy mechanism to call tools referenced within the source code to generate other source files. The tools library has a command called stringer which can create the String function automatically for an Enumeration type.

First you install the tool

$ go install golang.org/x/tools/cmd/stringer@latest

Then you add a magic comment to your enumeration code

package serverstatus

//go:generate stringer -type Enum

// Enum is an enumerated type
type Enum uint8

const (
    // Creating is an enumeration for serverstatus.Enum
    Creating Enum = iota + 1
...

and then you generate the code

$ go generate

This creates a file called enum_string.go which contains the String function. Now we can see what it does.

$ go test
--- FAIL: Example (0.00s)
got:
Deleting
want:
deleting
FAIL
exit status 1

Unfortunately stringer generates an exact textual copy of the constant as it is in the source code, which is fine for generating human consumed output, but no good for the JSON string we need. The Brightbox API expects snake case, not camel case in the enumerations.

So instead of tackling the Stringer interface directly, let’s delegate the problem to the JSON level within the TextMarshaler interface, and then the String function becomes straightforward and universal

// String makes Enum satisfy the Stringer interface
func (i Enum) String() string {
    tmp, err := i.MarshalText()
    if err == nil {
        return string(tmp)
    }
    return ""
}

Encoding to JSON

Go handles encoding a type to and from JSON string format via two interfaces: the TextMarshaler interface which encodes the type as a string and the TextUnmarshaler interface which decodes a string into the type.

Annoyingly the two interfaces are asymmetric in the way they handle errors. The marshalling system will wrap an error returned by TextMarshal in a MarshallerError type, whereas the unmarshal system leaves it up to the TextUnmarshaler implementer to wrap any errors in an UnmarsalTypeError.

Implementing the TextMarshal function is handled by a simple switch and error return.

// MarshalText implements the text marshaller method
func (i Enum) MarshalText() ([]byte, error) {
    switch i {
    case Creating:
        return []byte("creating"), nil
    case Active:
        return []byte("active"), nil
    case Inactive:
        return []byte("inactive"), nil
    case Deleting:
        return []byte("deleting"), nil
    case Deleted:
        return []byte("deleted"), nil
    case Failed:
        return []byte("failed"), nil
    case Unavailable:
        return []byte("unavailable"), nil
    }
    return nil, fmt.Errorf("%d is not a valid serverstatus.Enum", i)
}

The return direction is broken out into a standalone function that can create a type directly from a string

// ParseEnum attempts to convert a string into a Enum
func ParseEnum(name string) (Enum, error) {
    switch name {
    case "creating":
        return Creating, nil
    case "active":
        return Active, nil
    case "inactive":
        return Inactive, nil
    case "deleting":
        return Deleting, nil
    case "deleted":
        return Deleted, nil
    case "failed":
        return Failed, nil
    case "unavailable":
        return Unavailable, nil
    }
    var zero Enum
    return zero, fmt.Errorf("%s is not a valid serverstatus.Enum", name)
}

The TextUnmarshaler interface can then leverage ParseEnum in a standard fashion and wrap the error return appropriately, in the required UnmarshalText method

// UnmarshalText implements the text unmarshaller method
func (i *Enum) UnmarshalText(text []byte) error {
    name := string(text)
    tmp, err := ParseEnum(name)
    if err != nil {
        return &json.UnmarshalTypeError{
            Value: name,
            Type:  reflect.TypeOf(*i),
        }
    }
    *i = tmp
    return nil
}

Generating the Enum

Now we have the layout for Go enumerations, it’s but a small step to create a generator that will write the enumerations for us. The generate_enum script does just that. It takes the package name and a list of items in the enumeration as arguments and creates a new package in the enums directory relative to the package file containing the generate command. There’s even a -z option which will make the first time in the enumeration list the ‘zero value’ that is assigned automatically when a variable is declared.

It’s then trivial to generate a set of enumerations

//go:generate ./generate_enum loadbalancerstatus creating active deleting deleted failing failed
//go:generate ./generate_enum proxyprotocol v1 v2 v2-ssl v2-ssl-cn
//go:generate ./generate_enum balancingpolicy least-connections round-robin source-address
//go:generate ./generate_enum healthchecktype tcp http
//go:generate ./generate_enum listenerprotocol tcp http https

import them for use

import (
    "context"
    "path"
    "time"

    "github.com/brightbox/gobrightbox/v2/enums/balancingpolicy"
    "github.com/brightbox/gobrightbox/v2/enums/healthchecktype"
    "github.com/brightbox/gobrightbox/v2/enums/listenerprotocol"
    "github.com/brightbox/gobrightbox/v2/enums/loadbalancerstatus"
    "github.com/brightbox/gobrightbox/v2/enums/proxyprotocol"
)

and declare typed Enum items

// LoadBalancerListener represents a listener on a LoadBalancer
type LoadBalancerListener struct {
    Protocol      listenerprotocol.Enum `json:"protocol,omitempty"`
    In            uint16                `json:"in,omitempty"`
    Out           uint16                `json:"out,omitempty"`
    Timeout       uint                  `json:"timeout,omitempty"`
    ProxyProtocol proxyprotocol.Enum    `json:"proxy_protocol,omitempty"`
}

Try Brightbox for free

If you want to play with the Brightbox API, you can sign up for Brightbox real quick and use your £50 free credit to give it a go.

Get started with Brightbox Sign up takes just two minutes...