Introduction
Prior to coding in Go, I was writing software in C#. In C# enumerations can be declared and the associated type can be used in functions and as fields in a struct. The compiler won’t allow a value of the enumerated type to be passed or set that doesn’t belong to the defined set. This is something that I have missed since coding in Go.
Go doesn’t have enumerations and it can be a problem at times when you want type safety for a well-defined set of values. A common way people try to have enumerations in Go really doesn’t work for me because of the way constants behave. That being said, I have developed a pattern that gets me as close as possible to what I had in C# though it’s not perfect and I will explain.
Common Way
Let’s start with a common way people try to implement enumerations in Go which I avoid like the plague. I’ll explain why after you look at the code.
Listing 1
01 package main
02
03 import "fmt"
04
05 type Role string
06
07 const (
08 RoleAdmin = "ADMIN"
09 RoleUser = "USER"
10 )
11
12 func PrintRole(r Role) {
13 fmt.Println(r)
14 }
15
16 func main() {
17 PrintRole(RoleAdmin)
18 PrintRole("CUSTOMER")
19 }
Output:
ADMIN
CUSTOMER
Listing 1 shows a common solution to implementing enumerations in Go. The real problem is on line 18. There is no compiler protection because the compiler can implicitly convert the string to a Role
since it matches the underlying type of string
.
I want compiler protection and also make sure only the set of defined roles can be passed to PrintRole
.
Note: People have mentioned they like to use an integer type and iota for enumerations in Go. For the software I seem to write, these enumeration values are passed in web requests and need to be validated. Using a string instead of a number helps reduce confusion and increases readability. That being said, this would work with an integer value as well.
My Way
My way has a few moving parts, but it gives me everything I want with just two small flaws.
Listing 2
05 type Role struct {
06 name string
07 }
08
09 func (r Role) Name() string {
10 return r.name
11 }
Listing 2 shows the first change to the common way code. Instead of using an underlying type of string
for the Role
type, I use a struct. This struct has one unexported field called name
and it’s of type string
. Using a struct type will provide the compiler protections I want.
Note: Someone asked me why not replace the Name
method for the Stringer
interface. I think it’s a fair change, but I like having this Name
method which is more precise with getting the value for the enumeration.
Listing 3
13 var (
14 RoleAdmin = Role{"ADMIN"}
15 RoleUser = Role{"USER"}
16 )
Listing 3 shows the next two changes. Instead of using constants, I use variables to represent the set of roles. I need to use variables because constants can only be numeric, string, or bool.
Listing 4
18 func PrintRole(r Role) {
19 fmt.Println(r)
20 }
21
22 func main() {
23 PrintRole(RoleAdmin)
24 PrintRole("CUSTOMER")
25 }
Listing 4 uses the same code that was used in the common way. This time when I try to build the program, I get the error from the compiler I want.
Listing 5
cannot use "CUSTOMER" (untyped string constant) as Role value in argument to PrintRole
Restricting The Set
This is great, but I still need a way to create a Role
from a string
and make sure there can’t be a Role beyond the set that has been defined.
Listing 6
27 var roles = map[string]Role{
28 RoleAdmin.name: RoleAdmin,
29 RoleUser.name: RoleUser,
30 }
31
32 func ParseRole(value string) (Role, error) {
33 role, exists := roles[value]
34 if !exists {
35 return Role{}, fmt.Errorf("invalid role %q", value)
36 }
37
38 return role, nil
39 }
Listing 6 shows how to restrict the code to the defined set of roles. First, I define an unexported map with the defined set of roles and then I define a parse function. The parse function helps with validation and gives the code a guarantee a Role can’t be created with a string that isn’t associated with the defined set of roles.
The Flaws
Maybe you see the flaws already. One flaw is that a zero-valued Role
can be constructed which is not part of the defined set of roles. The other flaw is that RoleAdmin
and RoleUser
are now variables not constants, which allows the values to be changed outside of the package.
Listing 7
01 package main
02
03 import (
04 "fmt"
05 "internal/user"
06 )
07
08 func main() {
09 var r user.Role
10 user.RoleAdmin = user.Role{"CHANGED"}
11 }
Listing 7 shows how the flaws can be manipulated. Assuming the role enumeration is implemented in a package named user
, you can see the compiler won’t stop me from constructing a Role
set to its zero value and I could change a defined enumeration value. I could add a function to test if a Role
is zero, but in my experience this isn’t necessary. If anyone does change the enumeration value, I believe the code has bigger problems.
Marshaling / Unmarshalling
I will be using this type as a field in a struct so the field needs to be marshaled and unmarshalled. By implementing the TextMarshaler
and TextUnmarshaler
interfaces I get that support.
Listing 8
41 func (r *Role) UnmarshalText(data []byte) error {
42 role, err := ParseRole(string(data))
43 if err != nil {
44 return err
45 }
46
47 r.name = role.name
48 return nil
49 }
50
51 func (r Role) MarshalText() ([]byte, error) {
52 return []byte(r.name), nil
53 }
Listing 8 shows how to implement those interfaces. The cool thing with implementing these particular interfaces is that you get marshal and unmarshal support for all the different encoders that exist in the standard library.
If I want to test if these methods work with the JSON encoder, I can write this little test program.
Listing 9
16 func main() {
17 v := struct {
18 Role Role
19 }{
20 Role: RoleAdmin,
21 }
22
23 data, err := json.Marshal(v)
24 if err != nil {
25 fmt.Println(err)
26 return
27 }
28
29 fmt.Println(string(data))
30
31 good := []byte(`{"Role":"USER"}`)
32 if err := json.Unmarshal(good, &v); err != nil {
33 fmt.Println(err)
34 return
35 }
36
37 fmt.Printf("%#v\n", v)
38
39 bad := []byte(`{"Role":"BAD"}`)
40 if err := json.Unmarshal(bad, &v); err != nil {
41 fmt.Println(err)
42 return
43 }
44 }
Output:
{"Role":"ADMIN"}
struct { Role main.Role }{Role:main.Role{name:"USER"}}
invalid role "BAD"
Listing 9 shows a quick change to the main
function where the json.Marshal
and json.Unmarshal
functions are used to show how the implementation of the TextMarshaler
and TextUnmarshaler
interfaces work.
That’s it! Here is a complete view of the code.
Listing 10
01 package user
02
03 import "fmt"
04
05 var (
06 RoleAdmin = Role{"ADMIN"}
07 RoleUser = Role{"USER"}
08 )
09
10 var roles = map[string]Role{
11 RoleAdmin.name: RoleAdmin,
12 RoleUser.name: RoleUser,
13 }
14
15 type Role struct {
16 name string
17 }
18
19 func ParseRole(value string) (Role, error) {
20 role, exists := roles[value]
21 if !exists {
22 return Role{}, fmt.Errorf("invalid role %q", value)
23 }
24
25 return role, nil
26 }
27
28 func MustParseRole(value string) Role {
29 role, err := ParseRole(value)
30 if err != nil {
31 panic(err)
32 }
33
34 return role
35 }
36
37 func (r Role) Name() string {
38 return r.name
39 }
40
41 func (r *Role) UnmarshalText(data []byte) error {
42 role, err := ParseRole(string(data))
43 if err != nil {
44 return err
45 }
46
47 r.name = role.name
48 return nil
49 }
50
51 func (r Role) MarshalText() ([]byte, error) {
52 return []byte(r.name), nil
53 }
Listing 10 shows a complete view of the pattern for faking enumerations in Go with compiler protection and restrictions to the set of values. You will notice the MustParseRole
function which I did not mention in the post. I always add a Must
function to help with writing tests so I can skip the error handling. It’s a smell to use this Must
function outside of tests.
Conclusion
I love this pattern for implementing enumerations in Go. This has served me well and you can see how the code is used in the service repo. People have shared other ways they try to accomplish the same things in this post. So far, the code I’m presenting has been the simplest solution I’ve seen. I’m very open to different thoughts and ideas.