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.