Blog.

How to do Enums in Go

Marco Franssen

Marco Franssen /

11 min read2036 words

Cover Image for How to do Enums in Go

It has been a while since I wrote a blog on Go. Since I'm getting the question if Go supports enums every now and then, I thought it would be good to write an article on how to do enums in Go.

Go natively does NOT have an enum type like you might be used to from c# or Java. However that doesn't mean we can easily define our own type.

In this blog we will cover defining our own type, combined with a piece of code generation. If you are new to Go, then consider reading Start on your first Go project first.

Enums make up a nice API experience for consumers of your library by adding some type safety and giving the developer consuming the library some guidance on the available values.

Now we could use strings all over the place to pass our values, however using an enum is also more efficient as we will be using an integer to express and store these values in memory. You might be thinking… How can an integer be more convenient for the users of my library? Well, they aren't so let me show you how to get that Enum like developer experience in Go.

Imagine we are building a simple car configuration tool that allows to define the brand and color of a car. To offer a nice API for the consumer of our car package I want to offer them some predefined values (Enums) from which they can pick a brand and color. For the car model I go with a free format string as there are too many variations.

First lets have a look on the code I would like to write as a consumer of my car package.

main.go
package main
 
import (
  "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)'

By using an enum for the Brand and Color I get a very nice API to use my car package as a developer. It adds type safety as well it offers a predefined list of choices for brands and colors.

Now let's have a look on how we can define the struct and constructor function in our car package first.

car/car.go
package car
 
type 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,
  }
}

You notice that in this constructor I have used some custom types for Brand and Color. Lets have a look on how we can define these custom types and give them an enum like behavior.

car/brand.go
package car
 
type Brand int
 
const (
  BMW Brand = iota
  Mercedes
  Audi
  Toyota
  Volkswagen
  Porsche
  Ferrari
)

What we did here is defining a custom type Brand that is stored in memory as an integer. Then we define some predefined values as constants using iota. iota adds auto incremented values for a group of constants. Read more on iota here.

When compiling this code this will in practice result in the following values being assigned to the constant fields we defined.

| Field | Value | | ---------- | ----: | | BMW | 0 | | Mercedes | 1 | | Audi | 2 | | Toyota | 3 | | Volkswagen | 4 | | Porsche | 5 | | Ferrari | 6 |

Now you can do the same to define a Color type. Please go ahead and try that on your own. Ensure you define at least the colors Gray and Red to continue this tutorial.

Now if I would run my code we will notice it doesn't print that nicely as we have shown before.

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 are using %s with our fmt.Printf invocation to print our type as a String, therefore we need to implement the stringer interface to define how our Car prints as a string.

Interfaces in Go are implicit which means we don't need to define which interface we implement. We only need to add the function from the Stringer interface on our type.

To know more about interfaces in Go check out my other blog on interface and type assertions. Now lets add this function to our Car type.

car/car.go
func(c Car) String() string {
  return fmt.Sprintf("%s %s (%s)", c.Brand, c.Model, c.Color)
}

Although this prints already slightly better 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. However this time I don't want to manually implement this. I want to generate the implementation so I can easily update it when we are adding new constant values for theses types in the future.

To generate the implementation we will use a tool called stringer. We can install this tool using the following command.

terminal
go install golang.org/x/tools/cmd/stringer

Now in code we can add a //go:generate stringer -type=Brand statement to generate the String method on our type. I prefer to add this as a comment in front of my type definition, but in practice this can be anywhere in your package.

car/brand.go
package car
 
//go:generate stringer -type=Brand
type Brand int
 
const (
  BMW Brand = iota
  Mercedes
  Audi
  Toyota
  Volkswagen
  Porsche
  Ferrari
)

//go:generate is a special type of comment that that will be executed when we use go generate.

Same we will do for our Color type.

With the tool and the comment in place we can now simply run go generate ./.... This will create a new file called car/brand_string.go and a new file called car/color_string.go. Go ahead and have a look at the generated code. Now every time you changed your constants, e.g. added new values changed order or removed values, simply 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)'

Although this is already pretty neat, I would like to add one more thing to be able to marshal and unmarshal our values to and from JSON as a string. Lets start with marshalling to JSON. To do so we will implement the TextMarshaler interface on our Brand type.

car/brand.go
func (b Brand) MarshalText() ([]byte, error) {
  return []byte(b.String()), nil
}

Before we do the same for our Color type I suggest we will first try to print some JSON so we can see the difference if we don't implement this TextMarshaler interface.

Add following tags to your Car type so it nicely uses lowercased keys for our JSON.

car/car.go
type Car struct {
  Brand Brand  `json:"brand,omitempty"`
  Color Color  `json:"color,omitempty"`
  Model string `json:"model,omitempty"`
}

Lets now try to print our Car as JSON by adding a piece of code to our 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 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 we now nicely have our Brand printed as a string in our JSON, but the Color is still represented as an integer in our JSON. Go ahead and implement the TextMarshaler interface for your Color type.

Once 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"}]

So what if we would like 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 our Color type and add some code to our 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)

Now before popping a :beer: lets run our application one more time to see the result.

:warning: Please note if you forget to regenerate the stringer implementation for your types you will get something like following message.

terminal
$ go run .
# github.com/marcofranssen/go-enum-tutorial/car
car/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 :tada:, you made it to the end of this article. Want to learn more about Golang? Consider reading my other articles on Go.

Leave me a comment down below in the comments section.

TL;DR

The layout of our project looks like this.

terminal
$ tree .
.
├── car
   ├── brand.go
   ├── brand_string.go
   ├── car.go
   ├── color.go
   └── color_string.go
├── go.mod
└── main.go
go.mod
module github.com/marcofranssen/go-enum-tutorial

go 1.16
main.go
package main

import (
	"encoding/json"
	"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)

	cars := []*car.Car{bmw, ferrari}
	carsJSON, err := json.Marshal(cars)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(carsJSON))

	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\n", porsche)
}
car/car.go
package car

import "fmt"

type Car struct {
	Brand Brand  `json:"brand,omitempty"`
	Color Color  `json:"color,omitempty"`
	Model string `json:"model,omitempty"`
}

func New(brand Brand, color Color, model string) *Car {
	return &Car{
		Brand: brand,
		Color: color,
		Model: model,
	}
}

func (c Car) String() string {
	return fmt.Sprintf("%s %s (%s)", c.Brand, c.Model, c.Color)
}
car/brand.go
package car

import "strings"

//go:generate stringer -type=Brand
type Brand int

const (
	Unknown Brand = iota
	BMW
	Mercedes
	Audi
	Toyota
	Volkswagen
	Porsche
	Ferrari
)

func (b Brand) MarshalText() ([]byte, error) {
	return []byte(b.String()), nil
}

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
	}
}
car/car.go
package car

import "fmt"

type Car struct {
	Brand Brand  `json:"brand,omitempty"`
	Color Color  `json:"color,omitempty"`
	Model string `json:"model,omitempty"`
}

func New(brand Brand, color Color, model string) *Car {
	return &Car{
		Brand: brand,
		Color: color,
		Model: model,
	}
}

func (c Car) String() string {
	return fmt.Sprintf("%s %s (%s)", c.Brand, c.Model, c.Color)
}

Before running the application we will generate {brand,color}_string.go using the following command. We will also need to install the stringer generation tool first.

terminal
go install golang.org/x/tools/cmd/stringer
go 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.

References

Please leave me a comment below.

You have disabled cookies. To leave me a comment please allow cookies at functionality level.

More Stories

Cover Image for Install Hashicorp Vault on Kubernetes using Helm - Part 1

Install Hashicorp Vault on Kubernetes using Helm - Part 1

Marco Franssen

Marco Franssen /

In this blogpost I want to show you how to deploy Hashicorp Vault using Helm on Kubernetes. We will look at deploying on your local machine for development and experimental purposes but also at how to deploy a high available setup on AWS using Hashicorp Consul and automated unsealing using a AWS KMS key. I assume most of you will know about Hashicorp Vault, Helm, Kubernetes and Consul and therefore I will not go very much in details on the tools themself. In this first article of the series we…

Cover Image for Upgrade your SSH security

Upgrade your SSH security

Marco Franssen

Marco Franssen /

As a DevOps engineer you are probably familiar with SSH keys and how to use them already. I wrote some blogs on SSH in the past as well see the references. This time I want to zoom in a bit on the encryption strength of your keys and the encryption types you can use. Why should you care about this? In todays world password are becoming more and more a security risk. In the near future Github for example will not support password authentication anymore for clone, push and pull actions, just lik…

Cover Image for Hello Next.js, goodbye Hexo

Hello Next.js, goodbye Hexo

Marco Franssen

Marco Franssen /

For the folks reading my blog for a long time, you might have noticed I'm using my current theme and blogging engine for a long time. About 5 years ago I migrated from Wordpress to Hexo. Wordpress at that point in time was costing me serious money to get a decent performing webpage according to modern standards. So back then I decided to move into a statically generated blog, where I could write my blogs offline using markdown. Hexo has served me very well the last couple of years. It is a stat…

Cover Image for Remove files from Git history using git-filter-repo

Remove files from Git history using git-filter-repo

Marco Franssen

Marco Franssen /

Many of you have probably been in a situation where you committed a file in your repository which you shouldn't have done in the first place. For example a file with credentials or a crazy big file that made your repository clones very slow. Now there are a lot of blogs and guides already available on how to get these files completely removed. It involves git filter-branch or bfg sourcery. In this blog I'm going to show you the new recommended way of doing this using git-filter-repo, which simpl…