How to implement enums in Go with iota and stringer
Marco Franssen
10 min read•1851 words
0 likes0 reposts0 replies
Comments
Loading comments...
It's been a while since I wrote a post about Go. I still get asked whether Go supports enums, so let's look at how to build enums using custom types, constants, and code generation.
Go does NOT have a native enum type like you might be used to from c# or Java. However, that doesn't mean we can't define our own enum-like types.
In this post, we'll define our own type and combine it with a little code generation. If you're new to Go, consider reading Start on your first Go project first.
Enums make an API nicer for consumers of your library by adding some type safety and showing developers the available values.
We could use strings everywhere to pass our values. An enum is more efficient for storing these values in memory, though, because we'll use an integer to represent each value. You might be thinking… How can an integer be more convenient for users of my library? On its own, it isn't! Let me show you how to get that enum-like developer experience in Go.
Imagine we're building a simple car configuration tool that lets you define a car's brand and color. To make the API of our car package pleasant to use, I want to offer predefined values—enums—from which users can pick a brand and color. For the car model, I'll use a free-form string because there are too many variations.
First, let's look at the code I'd like to write as a consumer of my car package.
main.go
package mainimport ( "fmt" "github.com/marcofranssen/go-enum-tutorial/car")func main() { bmw := car.New(car.BMW, car.Gray, "320i GT") ferrari := car.New(car.Ferrari, car.Red, "SF90 Stadala") fmt.Printf("I own a '%s' and dream about a '%s'…\n", bmw, ferrari)}
Running this program should print the following output.
terminal
$ go run .I own a 'BMW 320i GT (gray)' and dream about a 'Ferrari SF90 Stadala (red)'…
Using enums for Brand and Color gives me a nice API for the car package. It adds type safety and offers a predefined list of brands and colors.
Let's start by defining the struct and constructor function in our car package.
car/car.go
package cartype Car struct { Brand Brand Color Color Model string}func New(brand Brand, color Color, model string) *Car { return &Car{ Brand: brand, Color: color, Model: model, }}
Notice that this constructor uses custom types for Brand and Color. Let's look at how to define them and give them enum-like behavior.
car/brand.go
package cartype Brand intconst ( BMW Brand = iota Mercedes Audi Toyota Volkswagen Porsche Ferrari)
Here, we've defined a custom type, Brand, with an integer as its underlying type. We then define named constants using iota. Within a group of constants, iota provides incrementing values. Read more about iotahere.
In practice, this assigns the following values to our constants.
Field
Value
BMW
0
Mercedes
1
Audi
2
Toyota
3
Volkswagen
4
Porsche
5
Ferrari
6
You can do the same to define a Color type. Go ahead and try it yourself. Make sure you define at least Gray and Red to continue this tutorial.
If we run the code now, we'll notice that the output isn't as nice as the example we saw earlier.
terminal
$ go run .I own a '&{%!s(main.Brand=0) %!s(main.Color=0) 320i GT}' and dream about a '&{%!s(main.Brand=6) %!s(main.Color=1) SF90 Stadala}'…
The reason for this ugly output is that our Car type does NOT implement the Stringer interface. We're using %s in our fmt.Printf call to print the type as a string, so we need to implement the stringer interface to define how our Car is represented as a string.
Interfaces in Go are satisfied implicitly, so we don't need to declare which interface we implement. We only need to add the method required by the Stringer interface to our type.
To learn more about interfaces in Go, check out my post on interfaces and type assertions. Now let's add this method to our Car type.
This looks slightly better, but we still don't get the desired output for our Brand and Color types.
terminal
$ go run .I own a '%!s(main.Brand=0) 320i GT (%!s(main.Color=0))' and dream about a '%!s(main.Brand=6) SF90 Stadala (%!s(main.Color=1))'…
Guess what?! We also need to implement the Stringer interface for our Brand and Color types. This time, I don't want to write the implementation by hand. I want to generate it so it's easy to update when we add new constants in the future.
To generate the implementation, we'll use a tool called stringer. We can install it with the following command.
terminal
go install golang.org/x/tools/cmd/stringer
Now we can add a //go:generate stringer -type=Brand directive to generate the String method for our type. I prefer to put this comment before the type definition, but it can go anywhere in your package.
car/brand.go
package car//go:generate stringer -type=Brandtype Brand intconst ( BMW Brand = iota Mercedes Audi Toyota Volkswagen Porsche Ferrari)
//go:generate is a special comment that specifies a command to run when we use go generate.
We'll do the same for our Color type.
With the tool and comments in place, run go generate ./.... This creates car/brand_string.go and car/color_string.go. Go ahead and look at the generated code. Whenever you change your constants—by adding values, changing their order, or removing values—run go generate ./... again.
If we run our program now, we finally get the desired output.
terminal
$ go run .I own a 'BMW 320i GT (Gray)' and dream about a 'Ferrari SF90 Stadala (Red)'…
This is already pretty neat, but I'd like to add one more thing: marshaling and unmarshaling our values to and from JSON as a string. Let's start with marshaling to JSON by implementing the TextMarshaler interface for our Brand type.
Before doing the same for Color, let's print some JSON to see what happens without the TextMarshaler interface.
Add the following tags to the fields of your Car type to use lowercase keys in the JSON.
car/car.go
type Car struct { Brand Brand `json:"brand,omitempty"` Color Color `json:"color,omitempty"` Model string `json:"model,omitempty"`}
Let's print our car as JSON by adding some code to the main function.
main.go
func main() { bmw := car.New(car.BMW, car.Gray, "320i GT") ferrari := car.New(car.Ferrari, car.Red, "SF90 Stadala") fmt.Printf("I own a '%s' and dream about a '%s'…\n", bmw, ferrari) cars := []*car.Car{bmw, ferrari} carsJSON, err := json.Marshal(cars) if err != nil { fmt.Println(err) } fmt.Println(string(carsJSON))}
Our program now shows the following output:
terminal
$ go run .I own a 'BMW Gray (320i GT)' and dream about a 'Ferrari Red (SF90 Stadala)'…[{"brand":"BMW","color":1,"model":"320i GT"},{"brand":"Ferrari","color":2,"model":"SF90 Stadala"}]
As you can see, Brand is now represented as a string in our JSON, but Color is still an integer. Go ahead and implement the TextMarshaler interface for your Color type.
Once that's done, you should see both brand and color as a string in the JSON.
terminal
$ go run .I own a 'BMW 320i GT (Gray)' and dream about a 'Ferrari SF90 Stadala (Red)'…[{"brand":"BMW","color":"Gray","model":"320i GT"},{"brand":"Ferrari","color":"Red","model":"SF90 Stadala"}]
What if we want to unmarshal JSON into our struct? For that, we need to implement the TextUnmarshaler interface.
car/brand.go
func (b *Brand) UnmarshalText(text []byte) error { *b = BrandFromText(string(text)) return nil}func BrandFromText(text string) Brand { switch strings.ToLower(text) { default: return Unknown case "bmw": return BMW case "mercedes": return Mercedes case "audo": return Audi case "toyota": return Toyota case "volkswagen": return Volkswagen case "porsche": return Porsche case "ferrari": return Ferrari }}
Do the same for Color, then add some code to the main function to create another car from a JSON string.
main.go
porscheJSON := []byte(`{"brand":"porsche","color":"White","model":"Taycan"}`) var porsche car.Car err = json.Unmarshal(porscheJSON, &porsche) if err != nil { fmt.Println(err) return } fmt.Printf("Another Car I love: %s", porsche)
Before popping a 🍺, let's run the application one more time to see the result.
⚠️ If you forget to regenerate the stringer implementation after changing your constants, you'll get a message like this.
terminal
$ go run .# github.com/marcofranssen/go-enum-tutorial/carcar/brand_string.go:11:7: invalid array index BMW - 0 (out of bounds for 1-element array)car/brand_string.go:12:7: invalid array index Mercedes - 1 (out of bounds for 1-element array)car/brand_string.go:13:7: invalid array index Audi - 2 (out of bounds for 1-element array)car/brand_string.go:14:7: invalid array index Toyota - 3 (out of bounds for 1-element array)car/brand_string.go:15:7: invalid array index Volkswagen - 4 (out of bounds for 1-element array)car/brand_string.go:16:7: invalid array index Porsche - 5 (out of bounds for 1-element array)car/brand_string.go:17:7: invalid array index Ferrari - 6 (out of bounds for 1-element array)
Go ahead and regenerate the code and run your application again.
terminal
$ go generate ./...$ go run .I own a 'BMW 320i GT (Gray)' and dream about a 'Ferrari SF90 Stadala (Red)'…[{"brand":"BMW","color":"Gray","model":"320i GT"},{"brand":"Ferrari","color":"Red","model":"SF90 Stadala"}]Another Car I love: Porsche Taycan (White)
Congratulations 🎉, you've made it to the end of this article. Want to learn more about Go? Check out my other Go articles.
Leave me a comment below with your questions or experiences.
Before running the application, we'll generate {brand,color}_string.go with the following commands. First, we need to install the stringer tool.
terminal
go install golang.org/x/tools/cmd/stringergo generate ./...
Now we can run our application.
terminal
$ go run .I own a 'BMW 320i GT (Gray)' and dream about a 'Ferrari SF90 Stadala (Red)'…[{"brand":"BMW","color":"Gray","model":"320i GT"},{"brand":"Ferrari","color":"Red","model":"SF90 Stadala"}]Another Car I love: Porsche Taycan (White)
Curious how it all works? Then read the full article.
Remove unwanted files from Git history with git-filter-repo, find previously deleted files, and push rewritten history using this practical walkthrough.