Go Enums: The Right Way to Implement, Iterate and Namespacing Trick
We'll talk about how to use enums in Go, covering everything from number groups to using open-source libraries and what 'iota' means.
Right now, Go (version 1.19) doesn’t have a built-in enum type, something that many other languages do offer.
But fortunately resourceful Go developers have found clever ways to work around this, using what the language already provides.
1. The Idiomatic Way
So you’re wondering how to make enums work in Go when it doesn’t even have a native enum type?
A common practice is to define a set of unique integers as constants and use those to represent the enum values
type Color int
const (
Red Color = iota // 0
Blue // 1
Green // 2
)
This method of using a group of unique integers to act as enum values is pretty standard in Go and the usual guidance is to start the enum values from 1, saving 0 for ‘undefined’ or a default state. It’s a straightforward way to have enums in Go.
In the example above, we didn’t set a default value, let’s say I don’t want the default color to be red when initialized.
In that case, a small tweak can be made:
const (
Red Color = iota + 1 // 1
Blue // 2
Green // 3
)
And if you really want, you can add an ‘Undefined’ member as the first initialized value, or any other color you consider default, like ‘Black’;
const (
Undefined Color = iota // 0
Red // 1
Blue // 2
Green // 3
)
“This seems too basic. Got anything more interesting?”
Absolutely, you can certainly stretch the functionality of the iota identifier. For example, if you’re interested in an enum sequence that starts at 2 and goes up by 2s, like 2, 4, 6, that’s doable:
const (
Red Color = (iota + 1) * 2 // 2
Blue // 4
Green // 6
)
We add 1 to iota here to make sure ‘Red
’ doesn’t start at 0, which would give us a sequence like 0, 2, 4 instead.
“Can I exclude a specific number from the sequence? Say, skip the number 2?”
Yes, you can.
In Go, the underscore (‘_’) can act as a placeholder to help you skip a value in your enum sequence when using iota.
const (
Red Color = iota + 1 // 1
_
Blue // 3
Green // 4
)
This way, the compiler knows to skip that spot, letting you leave out a value from the sequence.
2. Flags Enum Technique
This approach lets you store a bunch of different enum values as a single integer. In this setup, each bit in the integer corresponds to a specific enum value:
type DamageType int
const (
Poison DamageType = 1 << iota // 1
Bleeding // 2
Flame // 4
)
You can store multiple enum values in one integer and check if a certain enum value exists in that set, all using bitwise operations:
// create our bitwise utility, if c contains the damageType or not.
func (dt DamageType) Has(dmg DamageType) bool {
return dt&dmg == dmg
}
func main() {
damages := Poison | Flame
fmt.Println(damages.Has(Flame)) // true
fmt.Println(damages.Has(Bleeding)) // false
}
“Any alternatives for simulating flag-based enums?”
Absolutely.
While you can create your own custom functions, Go also offers the math/bits
package for bit manipulation.
Here’s a simpler solution that I use
// Has returns a boolean indicating whether the specific flag is present in the set
func Has(set, flag int) bool {
return set&flag == flag
}
// Remove creates a new set of flags with the specific flag removed and returns it
func Remove(set, flag int) int {
return set &^ flag
}
// Add returns a new set of flags with the specific flag added
func Add(set, flag int) int {
return set | flag
}
If you’re curious to understand, I’ve written an article on this flag enum method, which you can read: ‘What is Flags Enum and How to Implement It’.
3. Iterate over Enum
In the current version of Go (1.19), there’s no built-in way to loop through an enum. But you can get creative if you’re using iota to set up your enum sequence:
const (
StartColor Color = iota
Red
Blue
Green
EndColor
)
func main() {
for i := StartColor + 1; i < EndColor; i++ {
fmt.Println(i)
}
}
Output:
// 1
// 2
// 3
Still want more flexibility or interested in string-based enums? You have options. You might think about using an open-source package like go-enum, alongside the go generate
tool.
If you’re comfortable with the command line, go ahead and execute the command there.
Alternatively, if you’re on VSCode or a similar IDE with inline command support, you can just click on the command. This action will create a new file that outlines various details about your ‘Color’ enum.
Here’s how you can iterate through the enum in this setup:
func main() {
for i := range ColorNames() {
fmt.Println(i, Color(i))
}
}
// 0 red
// 1 green
// 2 blue
Thanks to the flag --names
, the function ColorNames()
is generated. There are additional flags to customize the setup, so you might want to experiment a bit.
If you’re interested but not keen on running the code yourself, here’s what the generated file includes:
// Code generated by go-enum DO NOT EDIT.
// Version:
// Revision:
// Build Date:
// Built By:
package medium
import (
"fmt"
"strings"
)
const (
// ColorRed is a Color of type Red.
ColorRed Color = iota
// ColorGreen is a Color of type Green.
ColorGreen
// ColorBlue is a Color of type Blue.
ColorBlue
)
var ErrInvalidColor = fmt.Errorf("not a valid Color, try [%s]", strings.Join(_ColorNames, ", "))
const _ColorName = "redgreenblue"
var _ColorNames = []string{
_ColorName[0:3],
_ColorName[3:8],
_ColorName[8:12],
}
// ColorNames returns a list of possible string values of Color.
func ColorNames() []string {
tmp := make([]string, len(_ColorNames))
copy(tmp, _ColorNames)
return tmp
}
var _ColorMap = map[Color]string{
ColorRed: _ColorName[0:3],
ColorGreen: _ColorName[3:8],
ColorBlue: _ColorName[8:12],
}
// String implements the Stringer interface.
func (x Color) String() string {
if str, ok := _ColorMap[x]; ok {
return str
}
return fmt.Sprintf("Color(%d)", x)
}
var _ColorValue = map[string]Color{
_ColorName[0:3]: ColorRed,
_ColorName[3:8]: ColorGreen,
_ColorName[8:12]: ColorBlue,
}
// ParseColor attempts to convert a string to a Color.
func ParseColor(name string) (Color, error) {
if x, ok := _ColorValue[name]; ok {
return x, nil
}
return Color(0), fmt.Errorf("%s is %w", name, ErrInvalidColor)
}
4. Enum Namespacing Trick
Jarrod Roberson commented, ‘In a package, everything ends up in the same namespace, making things hard to locate.’ His comment made me realize I’d faced this issue before but hadn’t included it in my original post.
Here’s a trick I use to make enum values easier to find and manage:
type Color int
var ColorEnum = struct {
Red Color
Blue Color
Green Color
}{
Red: 0,
Blue: 1,
Green: 2,
}
func main() {
fmt.Println(ColorEnum.Red)
}
By putting the enum values in an anonymous struct, you can more easily locate them in your IDE. But, be aware that this approach has a downside: it limits the use of iota, as far as I know.
“Any ideas on how to loop through these enums?”
If you find yourself needing to loop through all elements in ColorEnum
, you could write a utility function using reflection.
You're new to the concept of reflection? take a look at my previous post Reflection in Go: Everything You Need to Know to Use It Effectively
If you discover a better or simpler method for handling enums, feel free to share it with me in the comments.