Compile-time safety for enumerations in Go

Last month, a colleague of mine asked how to reason about enumerated values (“enums”) in Go. They wanted to benefit from Go’s type-safety and to prevent users from misusing the package.

For an imaginary example, that enumerates possible colours, the snippet below shows how we typically implement this:

type Color string

const (
    Red   Color = "red"
    Green Color = "green"
    Blue  Color = "blue"
)

But, just like Bill Kennedy illustrated it in the blog post on a similar topic, such implementation allows a user to pass any “untyped constant string” in places where the Color type is expected. Also, a user can declare their own value of a type Color, going beyond what was defined by the initial enumeration set.

package main

func main() {
    PrintColor(color.Red)

    PrintColor("RAINBOW")

    var rainbow color.Color = "🌈"
    PrintColor(rainbow)
}

func PrintColor(c color.Color) {
    fmt.Printf("%v\n", c)
}

// Outputs:
red
RAINBOW
🌈

Note how in the snippet above, the user both called PrintColor(“RAINBOW”) and defined their own variable of type color.Color, which holds a random string.

Update: a number of people pointed at my choice of words in the paragraph below, where I introduce a way to solve the problems. Indeed, “elegant” isn’t the most accurate one to describe this solution :) Also, refer to #19412 for the discussion about adding sum types to Go.


Go’s type system allows preventing both issues in a rather elegant way. Let’s declare the Color interface, with an unexported method, while implementing this interface in an unexported type color:

package color

type Color interface {
    xxxProtected()
}

type color string

func (c color) xxxProtected() {}

const (
	Red   color = "red"
	Green color = "green"
	Blue  color = "blue"
)

Because no types outside of our package color can implement the unexported method color.Color.xxxProtected(), we limit the possible implementations of the Color interface to only values, defined in the package color.

package main

func main() {
    PrintColor(color.Red)

    //PrintColor("RAINBOW") // cannot use (constant of type string) as color.Color
}

func PrintColor(c color.Color) {
    fmt.Printf("%v\n", c)
}

If a user tries to pass an untyped constant string or declare their own variant of the colour, the code won’t compile, resulting with an error:

package main

type mycolor string

func (c mycolor) xxxProtected() {}

func main() {
    black := mycolor("BLACK")
    PrintColor(black)
}

OUTPUTS:
./prog.go:9:13: cannot use black (variable of type mycolor) as color.Color value in argument to PrintColor:
mycolor does not implement color.Color (missing method xxxProtected)

Have you stumbled upon a scenario, where such strict compile-time checks for enumerated values was needed? Share your thoughts with me on Hacker News, Twitter or Bluesky.

All topics

applearduinoarm64askmeawsberlinbookmarksbuildkitcgocoffeecontinuous profilingCOVID-19designdockerdynamodbe-paperenglishenumesp8266firefoxgithub actionsGogooglegraphqlhomelabIPv6k3skuberneteslinuxmacosmaterial designmDNSmusicndppdneondatabaseobjective-cpasskeyspostgresqlpprofprofeferandomraspberry pirusttravis civs codewaveshareµ-benchmarks