Skip to main content
Preslav Rachev

Image Credits: Stable Diffusion via lexica.art

Q: Can I have a fully-functioning enum in Go, similar to XYZ (my other programming language of choice)?

A: No. Proper enums do not exist in Go, but you can come pretty close if you pull a few tricks.

NOTE: After reading the post, make sure to check out this comment on Reddit and the discussion we started around it. There may be an even better approach to achieving type-safe enums in Go thanks to generic type set interfaces. I will look it amore carefully and promise to write a part 2 of this blog post.

One of Go’s most requested features, after generic type parameters and more ergonomic error handling, has been the addition of enums. And to those who would point out that Go already has enums, I’d say no, the iota hack is far from sufficient.

const (
	Motorcycle = iota
	Car
	Bus
	Truck
)

It’s so easy to mess up by comparing any of the above with a random integer (or any other primitive value) that I don’t even want to talk about it.

Someone could say, well, create a new type based on a primitive and then use that type everywhere.

type Vehicle int

const (
	Motorcycle Vehicle = iota
	Car
	Bus
	Truck
)

That’s one idea better. Now, we can at least make sure that people put some reasoning when working with our “enum”:

func calculateInsuranceRate(v Vehicle) float64 {
	// ...
}

However, what could stop one from calling the function like this:

rate := calculateInsuranceRate(Vehicle(42)) // Ouch!

What is even worse: what happens if 42 becomes a valid value one day? Our code might end up working in mysterious ways without us even knowing about it or why.

A safer, more robust enum, based on interfaces #

An enum has one distinct property - it has a finite number of values, whereas new ones get accepted only if they are a part of the enum. In addition, if you look at enums in other programming languages (e.g, Java, Rust, Swift, Kotlin, etc.), they are far more comprehensive than simply a scalar value that one compares against. Taken to the extreme, an enum is a form of finite-state machine where each value encapsulates its own state and behavior. I guess, you know where I am going - interfaces.

Let’s create a separate package reserved explicitly for our enum. Let’s call it vehicle. Inside vehicle/vehicle.go, add the following interface definition:

type Type interface {
	isEnumValue()
}

This is an exported interface, which can be implemented only from inside the package. This way, we can somewhat prevent the creation of new enum values (but not eliminate it entirely, read on).

Next to our interface, we can declare four structs, representing all the enum options:

type car struct {
	// potentially, add some fields here to keep some state
}

type motorcycle struct {
	// potentially, add some fields here to keep some state
}

type bus struct {
	// potentially, add some fields here to keep some state
}

type truck struct {
	// potentially, add some fields here to keep some state
}

func (_ *motorcycle) isEnumValue() {}
func (_ *car) isEnumValue()        {}
func (_ *bus) isEnumValue()        {}
func (_ *truck) isEnumValue()      {}

Notice that those are also un-exported, so that no outside code can instantiate them at will.

To make those publicly accessible, all we have to do is declare a struct at the package level that exposes them:

var (
	Values = struct {
		Motorcycle *motorcycle
		Car        *car
		Bus        *bus
		Truck      *truck
	}{
		Motorcycle: &motorcycle{},
		Car:        &car{},
		Bus:        &bus{},
		Truck:      &truck{},
	}
)

And there you have it! All an outside package would see now is that there is a public interface called vehicle.Type , but it cannot be implemented, and the only way to provide a working implementation of it is to use one of the vehicle.Values :

func calculateInsuranceRate(v vehicle.Type) (float64, error) {
	switch v {
	case vehicle.Values.Motorcycle:
		return 0.05, nil
	case vehicle.Values.Car:
		return 0.2, nil
	case vehicle.Values.Bus:
		return 0.3, nil
	case vehicle.Values.Truck:
		// We can even invoke some methods on the concrete value
		return 0.3 * vehicle.Values.Truck.FetchSomeData(), nil
	default:
		return 0, errors.New("vehicle type undefined")
	}
}

That’s quite a lot of boilerplate, I know. The upside of that is that you only need to pay the price of it once, and remember to add new values accordingly. Or, you can use a code generator to do it for you. And you can add all kinds of methods to each of your value types so that you can convert from and back to scalar values (e.g. for serialisation purposes), or do all kinds of stuff.

But beware … #

Now for the caveat. Some of you might be asking themselves, well, why don’t we add those methods directly to the vehicle.Type interface. It will save us quite some boilerplate with all that checking - you could simply do:

return v.CalculateInsuranceRate()

and be done with it, right?

Well, yes, but that also opens the door for potential misuse of the enum. If you recall one of my previous blog posts, you will remember that you can make a Go type pass for an interface, without really implementing it. Unfortunately, that applies to the un-exported methods too. Thus, the following piece of code is perfectly valid:

type FakeVehicle {
	vehicle.Type
}

rate, err := calculateInsuranceRate(&FakeVehicle{})

which is why I’d prefer having the interface as small as possible, and use concrete methods or package level functions that guard against mis-use.

What do you think? Will you ever use this construct in your Go code? Feel free to let me know in the comments below.

Inspiration #

Go and Algebraic Data Types
Algebraic data types (also known as variant types, sum types or discriminated unions) is a neat feature of some programming languages that lets us specify that a value might take one of several related types, and includes convenient syntax for pattern matching on these types at run-time. Here's a canonical binary tree example from Wikipedia, written in Haskell:
Safer Enums in Go
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 ) Full source: github.com/ThreeDotsLabs/go-web-app-antipatterns/02-enums/01-iota/role/role.go While Go is explicit, iota seems relatively obscure.
Abuse of Sum Types In OO Languages
Sum types are useful, but they are also an attractive nuisance in object oriented languages. There's a certain type of programmer who gets a taste of functional programming and has a good time, but misinterprets that good time to mean that sum types are always better, because they are FP, and FP is Better.

Have something to say? Join the discussion below 👇

Want to explore instead? Fly with the time capsule 🛸

You may also find these interesting