Enums are a crucial part of web applications. Go doesn’t support them out of the box, but there are ways to emulate them.

Many obvious solutions are far from ideal. Here are some ideas we use that make enums safer by design.

iota

Go lets you enumerate things with iota.

const (
	Guest = iota
	Member
	Moderator
	Admin
)

While Go is explicit, iota seems relatively obscure. If you sort the const group any other way, you introduce side effects. In the example above, you could accidentally create moderators instead of members. You can explicitly assign a number to each value to avoid this issue, but it makes iota obsolete.

iota works well for flags represented by powers of two.

const (
	Guest = 1 << iota // 1
	Member            // 2
	Moderator         // 4
	Admin             // 8
)

// ...

user.Roles = Member | Moderator // 6

Bitmasks are efficient and sometimes helpful. However, it’s a different use case than enums in most web applications. Often, you’ll be fine storing all roles in a list. It’ll also be more readable.

The main issue with iota is it works on integers that don’t guard against passing invalid values.

func CreateUser(role int) error {
	fmt.Println("Creating user with role", role)
	return nil
}

func main() {
	err := CreateUser(-1)
	if err != nil {
		fmt.Println(err)
	}
	
	err = CreateUser(42)
	if err != nil {
		fmt.Println(err)
	}
}

CreateUser will happily accept -1 or 42 even though there are no corresponding roles.

Of course, we could validate this within the function. But we use a language with strong types, so let’s take advantage of it. In our application’s context, a user role is much more than a vague number.

We could introduce a type to improve the solution.

type Role uint

const (
	Guest Role = iota
	Member
	Moderator
	Admin
)

It looks better, but it’s still possible to pass any arbitrary integer in place of Role. The Go compiler doesn’t help us here.

func CreateUser(role Role) error {
	fmt.Println("Creating user with role", role)
	return nil
}

func main () {
	err := CreateUser(0)
	if err != nil {
		fmt.Println(err)
	}
	
    err = CreateUser(role.Role(42))
    if err != nil {
        fmt.Println(err)
    }
}

The type is an improvement over a bare integer, but it’s still an illusion. It doesn’t give us any guarantee that the role is valid.

Sentinel values

Because iota starts from 0, the Guest is also the Role’s zero-value. It makes it hard to detect if the role is empty or someone passed a Guest value.

You can avoid this by counting from 1. Even better, keep an explicit sentinel value you can compare and can’t mistake for an actual role.

const (
	Unknown Role = iota
	Guest
	Member
	Moderator
	Admin
)
func CreateUser(r role.Role) error {
	if r == role.Unknown {
		return errors.New("no role provided")
	}
	
	fmt.Println("Creating user with role", r)
	
	return nil
}

Slugs

Enums seem to be about sequential integers, but it’s rarely a valid representation. In web applications, we use enums to group possible variants of some type. They don’t map well to numbers.

It’s hard to understand the context when you see a 3 in the API response, a database table, or logs. You have to check the source or the outdated documentation to know what it’s about.

String slugs are more meaningful than integers in most scenarios. Wherever you see it, a moderator is obvious. Since iota doesn’t help us anyway, we can as well use human-readable strings.

type Role string

const (
	Unknown   Role = ""
	Guest     Role = "guest"
	Member    Role = "member"
	Moderator Role = "moderator"
	Admin     Role = "admin"
)

Slug are especially useful for error codes. An error response like {"error": "user-not-found"} is obvious in contrast to {"error": 4102}.

However, the type can still hold any arbitrary string.

err = CreateUser("super-admin")
if err != nil {
	fmt.Println(err)
}

Struct-based Enums

The final iteration uses structs. It lets us work with code secure by design. We don’t need to check if the passed value is correct.

type Role struct {
	slug string
}

func (r Role) String() string {
	return r.slug
}

var (
	Unknown   = Role{""}
	Guest     = Role{"guest"}
	Member    = Role{"member"}
	Moderator = Role{"moderator"}
	Admin     = Role{"admin"}
)

Because the slug field is unexported, it’s not possible to fill it from outside the package. The only invalid role you can construct is the empty one: Role{}.

We can add a constructor to create a valid role based on a slug:

func FromString(s string) (Role, error) {
	switch s {
	case Guest.slug:
		return Guest, nil
	case Member.slug:
		return Member, nil
	case Moderator.slug:
		return Moderator, nil
	case Admin.slug:
		return Admin, nil
	}

	return Unknown, errors.New("unknown role: " + s)
}

This approach is perfect when you work with business logic. Keeping structures always in a valid state in memory makes your code easier to work with and understand. It’s enough to check if the enum type isn’t empty, and you’re sure it’s a correct value.

There’s one potential issue with this approach. Structs cannot be constants in Go, so it’s possible to overwrite the global variables like this:

roles.Guest = role.Admin

There is no sane reason to do this, though. You’re much more likely to pass an invalid integer by accident.

Another disadvantage is you have to make updates in two places: the enums list and the constructor. However, it’s easy to spot, even if you miss it at first.

How do you do it?

When there’s a missing Go feature, we tend to develop our ways of doing things. What I described is just one possible approach. Do you use another pattern to store your enums? Please share it in the comments. 🙂

For complete source code, see the anti-patterns repository.