Enums in Go: An Alternative approach
Introduction
Go programmers often miss the built‑in, "sealed" enumerations (enum) types that languages like Java or C# provide.
In Go, developers typically emulate enums by defining a new custom type and a set of related constants (often using the iota
identifier).
In this post, I'll show you how to emulate enums in Go using custom types and constants and enhance them with methods and code generation.
Tip
What is an enum? An enumeration (enum) is a distinct type consisting of a set of named constants called elements or enumerators. The enum type restricts variables to only those values defined in the enumeration. Enums are useful for defining a fixed set of related values.
Why Go doesn't have enums
Go's design philosophy emphasizes simplicity and minimalism. While traditional enums provide type safety and a fixed set of values, Go opts for the flexibility of constant declarations. This approach aligns well with the language’s overall design while allowing you to define a clear set of related values.
Using constants as enums
The most common pattern to emulate enums in Go is defining a new custom type (usually based on int
) and a set of related constants.
The iota
identifier is often used to create a sequence of related values.
Here's an example of how to define a custom type and constants to represent a limited set of programming languages:
package main
import "fmt"
type Language int
const (
Unknown Language = iota // 0
Go // 1
Java // 2
Python // 3
JavaScript // 4
)
func main() {
var lang Language = Go
fmt.Printf("The language is: %v\n", lang) // Output: The language is: 1
}
In this example, we define a new custom-type Language
based on int
and a set of related constants.
Tip
What is iota?
The iota
identifier is a predeclared identifier that represents successive integer constants.
It resets to 0
whenever the const
block is entered and increments by 1
for each subsequent constant declaration.
Advantages of using constants as enums include:
- Type safety: The custom-type
Language
restricts variables to only those values defined in the enumeration. - Readability: Constants provide descriptive names for each value, making the code more readable.
- Uniqueness: The
iota
identifier ensures that each constant is unique, preventing accidental reuse of values. - Maintainability: Constants are easy to update and maintain, allowing you to add or remove values as needed without needing to update each constant individually.
Adding behavior to enums with methods
While constants can represent a fixed set of related values, they lack the ability to define behavior. One advantage of declaring a custom type is that you can attach methods to it.
Here's an example of how to add a String
method to the Language
type to return the name of the language:
func (l Language) String() string {
return [...]string{"Unknown", "Go", "Java", "Python", "JavaScript"}[l]
}
func main() {
var lang Language = Go
fmt.Printf("The language is: %v\n", lang) // Output: The language is: Go
}
This simple String()
method approach makes it easy to convert the Language
type to a human-readable string.
Enhancing enums with code generation
As your enum grows, maintaining the list of constants and associated methods can become cumbersome and error-prone. If you add or remove values to the enum, you must remember to update the list of constants and any associated methods accordingly.
One way to address this problem is by using code generation tools like stringer
to generate the String
method for your custom type automatically.
Here's how you can use stringer
to generate the String
method for the Language
type:
Start by installing the stringer
tool:
go get golang.org/x/tools/cmd/stringer@latest
Then, add a //go:generate
directive to your source file to run the stringer
tool automatically with go generate
:
package main
import "fmt"
//go:generate stringer -type=Language -linecomment
type Language int
const (
Unknown Language = iota
Go // Golang
Java
Python
JavaScript
)
func main() {
var lang Language = Go
fmt.Printf("The language is: %v\n", lang) // Output: The language is: Golang
}
By running go generate ./...
in the same directory as your source code, stringer
will generate a String
method for the Language
type in a new language_string.go
file.
go generate
This approach simplifies the process of adding or removing values from your enum, as the String
method is automatically updated whenever you run go generate
.
Handling JSON serialization with enums
By default, if you marshal a struct containing your enum, Go will represent the enum as its underlying integer value.
To customize the JSON representation of your enum, you can implement the json.Marshaler
and json.Unmarshaler
interfaces.
Here's an example of how to implement the json.Marshaler
and json.Unmarshaler
interfaces for the Language
type:
import (
"encoding/json"
"fmt"
)
func (l Language) MarshalJSON() ([]byte, error) {
return json.Marshal(l.String())
}
func (l *Language) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
switch s {
case "Go":
*l = Go
case "Java":
*l = Java
case "Python":
*l = Python
case "JavaScript":
*l = JavaScript
default:
*l = Unknown
}
return nil
}
The MarshalJSON
method is used by the JSON encoder to serialize the Language
type.
The UnmarshalJSON
method is used by the JSON decoder to deserialize the Language
type.
By implementing these methods, you can control how your enum is represented in JSON format.
Limitations and trade-offs
While using constants as enums is a common pattern in Go, it has some limitations:
- Type Safety: Despite having a distinct type, nothing stops a programmer from casting an arbitrary integer to your enum type.
- Exhaustiveness: There's no built-in way to ensure that a
switch
statement covers all enum values. - Verbosity: Defining a custom type and constants for each enum can be verbose, especially for large enums.
- Duplication: When additional behavior is required (like parsing from a string), you may end up writing similar code for each enum type.
Check the
UnmarshalJSON
method in the previous section for an example.
Conclusion
While Go doesn't provide built-in enums, you can effectively emulate them using custom types and constants.
By attaching methods to your custom type and using code generation tools like stringer
, you can enhance your enums with additional behavior and simplify maintenance.
By following these patterns, you can effectively incorporate enum-like functionality into your Go projects, providing type safety and a clear set of related values to make your code both robust and readable.
Happy coding!