
Mastering Enums in Go: Best Practices and Implementation Patterns
This blog is also available as a video lesson that you can watch for free here
If you’re coming to Go from languages like Java, C#, or TypeScript, you might be wondering - “Where are all the enums?” Go doesn’t have built-in enumerated types, but that doesn’t mean we can’t implement powerful enum-like patterns. In fact, Go’s approach gives us some flexibility that traditional enums don’t.
Let’s dive into how to create robust enumerations in Go, with some hard-earned wisdom on best practices that will save you from subtle bugs.
Creating Enum-like Types in Go
Go doesn’t have enums directly, but we can use type constants to achieve similar functionality:
type Weekday int
const (
WeekdayInvalid Weekday = iota // 0
WeekdaySunday // 1
WeekdayMonday // 2
WeekdayTuesday // 3
WeekdayWednesday // 4
WeekdayThursday // 5
WeekdayFriday // 6
WeekdaySaturday // 7
)
What’s happening here?
- We define a custom type
Weekday
based on anint
- We create a block of constants of this type
- We use the special
iota
identifier, which increments automatically within a const block
The iota
identifier is super useful - it’s a pre-declared identifier that represents the untyped integer ordinal number (0-indexed) of the current const specification in a const block. In plain English, it just counts up from 0 as you define more constants.
The Zero Value Pattern - A Critical Best Practice
Notice something peculiar about our enum? The first value is WeekdayInvalid
rather than starting with Sunday. This is intentional and honestly one of the most important best practices when working with enum-like types in Go.
Why? Because in Go, the zero value for an int is 0. If you were to declare a variable of type Weekday
without initializing it:
var day Weekday // day will be 0
If we had used 0 for Sunday, then any uninitialized Weekday
variable would be automatically considered Sunday - potentially causing subtle logic bugs that are hard to track down.
Instead, we deliberately reserve the zero value (0) for an invalid state. This makes your code more robust by ensuring that:
- The zero value is not a valid option
- You need to deliberately set a valid value
- You can easily check if a value has been properly initialized
This pattern follows Go’s philosophy of making the zero value useful. In this case, “useful” means “safely detectable as uninitialized.”
Adding String Representation
Numeric values aren’t very human-readable. Let’s add a String()
method to make our enum more user-friendly:
func (w Weekday) String() string {
switch w {
case WeekdaySunday:
return "Sunday"
case WeekdayMonday:
return "Monday"
case WeekdayTuesday:
return "Tuesday"
case WeekdayWednesday:
return "Wednesday"
case WeekdayThursday:
return "Thursday"
case WeekdayFriday:
return "Friday"
case WeekdaySaturday:
return "Saturday"
default:
return "Invalid day"
}
}
This method is special in Go - by implementing the String()
method, our type satisfies the Stringer
interface from the fmt
package. This means when we use our enum in string formatting contexts, Go will automatically call this method:
fmt.Printf("Today is %s\n", WeekdayMonday) // Prints: Today is Monday
You don’t need to explicitly call the String() method - it happens automatically with the %s
format verb.
Working with Enum Values
One of the advantages of enum-like types in Go is that you can write methods that operate on them. Let’s add some useful functionality:
func (w Weekday) IsWeekend() bool {
switch w {
case WeekdaySaturday, WeekdaySunday:
return true
case WeekdayMonday, WeekdayTuesday, WeekdayWednesday, WeekdayThursday, WeekdayFriday:
return false
default:
panic("Invalid weekday value")
}
}
Now we can do things like:
func main() {
sunday := WeekdaySunday
monday := WeekdayMonday
fmt.Printf("%s is on the weekend: %t\n", sunday, sunday.IsWeekend())
fmt.Printf("%s is on the weekend: %t\n", monday, monday.IsWeekend())
var invalidDay Weekday // Zero value is WeekdayInvalid (0)
fmt.Printf("The name of day value %d is: %s\n", invalidDay, invalidDay)
// This would panic:
// invalidDay.IsWeekend()
}
This will output:
Sunday is on the weekend: true
Monday is on the weekend: false
The name of day value 0 is: Invalid day
Notice how the panic in IsWeekend()
provides a safeguard against using invalid enum values. In production code, you might want to handle this more gracefully depending on your requirements, but it ensures invalid states don’t silently propagate through your system.
Organizing Multiple Enum Types
When working with multiple enum types, keep them organized in separate const blocks to make it clear which constants belong together:
type Weekday int
const (
WeekdayInvalid Weekday = iota
WeekdaySunday
WeekdayMonday
WeekdayTuesday
WeekdayWednesday
WeekdayThursday
WeekdayFriday
WeekdaySaturday
)
type Month int
const (
MonthInvalid Month = iota
MonthJanuary
MonthFebruary
MonthMarch
// ... and so on
)
This separation makes your code more maintainable and reduces the chance of accidental mixups between different enum types.
Complete Example
Let’s put it all together with a complete, runnable example:
package main
import "fmt"
type Weekday int
const (
WeekdayInvalid Weekday = iota
WeekdaySunday
WeekdayMonday
WeekdayTuesday
WeekdayWednesday
WeekdayThursday
WeekdayFriday
WeekdaySaturday
)
func (w Weekday) String() string {
switch w {
case WeekdaySunday:
return "Sunday"
case WeekdayMonday:
return "Monday"
case WeekdayTuesday:
return "Tuesday"
case WeekdayWednesday:
return "Wednesday"
case WeekdayThursday:
return "Thursday"
case WeekdayFriday:
return "Friday"
case WeekdaySaturday:
return "Saturday"
default:
return "Invalid day"
}
}
func (w Weekday) IsWeekend() bool {
switch w {
case WeekdaySaturday, WeekdaySunday:
return true
case WeekdayMonday, WeekdayTuesday, WeekdayWednesday, WeekdayThursday, WeekdayFriday:
return false
default:
panic("Invalid weekday value")
}
}
func DayType(day Weekday) string {
switch day {
case WeekdaySaturday, WeekdaySunday:
return "weekend"
case WeekdayMonday, WeekdayTuesday, WeekdayWednesday, WeekdayThursday, WeekdayFriday:
return "weekday"
default:
panic("Unknown day type")
}
}
func main() {
// Using the enums
sunday := WeekdaySunday
monday := WeekdayMonday
fmt.Printf("Invalid day's value is: %d\n", WeekdayInvalid)
fmt.Printf("Sunday's value is: %d\n", sunday)
// String representation is called automatically with %s
fmt.Printf("Sunday's name: %s\n", sunday)
fmt.Printf("Invalid day's name: %s\n", WeekdayInvalid)
// Using the day type function
fmt.Printf("%s is a %s\n", monday, DayType(monday))
fmt.Printf("%s is a %s\n", sunday, DayType(sunday))
// Zero value demonstration
var unsetDay Weekday
fmt.Printf("An uninitialized Weekday has value: %d, which is: %s\n",
unsetDay, unsetDay)
// Trying to use an invalid value would panic
// Uncommenting the next line would cause a panic:
// fmt.Println(DayType(unsetDay))
}
Running this code produces:
Invalid day's value is: 0
Sunday's value is: 1
Sunday's name: Sunday
Invalid day's name: Invalid day
Monday is a weekday
Sunday is a weekend
An uninitialized Weekday has value: 0, which is: Invalid day
Key Takeaways
When implementing enum-like types in Go:
-
Reserve zero for invalid state: Always make the zero value (0) represent an invalid state to prevent accidental usage of uninitialized variables.
-
Implement the String() method: Add a String() method to make your enum values human-readable, especially in logs and error messages.
-
Use type-specific constants: Define constants of your specific type, not just plain integers, to get compile-time type checking.
-
Group related enums: Keep related enum constants together in const blocks, and separate different enum types into their own blocks.
-
Add helper methods: Implement methods on your enum types to encapsulate related behavior.
-
Validate input: When receiving enum values from external sources, validate them to ensure they are within the expected range.
-
Write tests: Always test enum behavior, especially corner cases involving the invalid (zero) value.
Beyond Basic Enums
While this covers the fundamental approach to enums in Go, there are more advanced patterns such as:
- Using bit flags for enums that can be combined
- Implementing parsing functions to convert from strings to enum values
- Using code generation tools to automate the creation of enum types and their methods
But the basic pattern shown here will serve you well for most use cases in Go.
By respecting Go’s idioms and making deliberate choices about how we represent enumerated types, we can create clean, type-safe code that leverages the compiler to catch errors early.
If you enjoyed this blog, consider checking out our video lesson too
Resources: