Imagine you’re building a Todo application using Go. Each task in your application has a status associated with it, indicating whether it’s “completed”, “archived” or “deleted”.
As a developer, you want to ensure that only valid status values are accepted when creating or updating tasks via your API endpoints. Additionally, you want to provide clear error messages to users if they attempt to set an invalid status apart from the valid ones.
Of course, you will say enums, right?
But the bad news is Go doesn’t provide enums and the good news is we can still implement it.
In this blog, we’ll explore how to effectively manage enums in Golang, including validation techniques to ensure data integrity and reliability.
Whether you are a beginner or an experienced developer, this guide aims to demystify enums and empower you to leverage their power in your Go projects.
We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
Enum is a short form of enumeration, which represents a distinct set of named constants. While languages like Java and C++ offer native support for enums, Go takes a different approach.
Instead of a dedicated enum type, Go developers use constants or custom types with iota to emulate enum-like behavior. Let’s begin!
Constants are a straightforward way to define enums in Go. Let’s consider a basic example where we define enum constants representing different days of the week.
package main
import "fmt"
const (
SUNDAY = "Sunday"
MONDAY = "Monday"
TUESDAY = "Tuesday"
WEDNESDAY = "Wednesday"
THURSDAY = "Thursday"
FRIDAY = "Friday"
SATURDAY = "Saturday"
)
func main() {
day := MONDAY
fmt.Println("Today is ", day) // "Today is Monday"
}
While constants work well for simple enums, custom types with iota provide a more idiomatic approach in Go.
iota
is a special identifier in Go that is used with const
declarations to generate a sequence of related values automatically.
It simplifies the process of defining sequential values, particularly when defining enums or declaring sets of constants with incrementing values.
When iota
is used in a const
declaration, it starts with the value 0 and increments by 1 for each subsequent occurrence within the same const
block. If iota
appears in multiple const
declarations within the same block, its value is reset to 0 for each new const
block.
package main
import "fmt"
const (
SUNDAY = iota // SUNDAY is assigned 0
MONDAY // MONDAY is assigned 1 (incremented from SUNDAY)
TUESDAY // TUESDAY is assigned 2 (incremented from MONDAY)
WEDNESDAY // WEDNESDAY is assigned 3 (incremented from TUESDAY)
THURSDAY // THURSDAY is assigned 4 (incremented from WEDNESDAY)
FRIDAY // FRIDAY is assigned 5 (incremented from THURSDAY)
SATURDAY // SATURDAY is assigned 6 (incremented from FRIDAY)
)
func main() {
fmt.Println(MONDAY, WEDNESDAY, FRIDAY) // Output: 1 3 5
}
Let’s rewrite the previous example using a custom type(int).
package main
import "fmt"
type Day int
const (
Sunday Day = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
func main() {
day := Monday
fmt.Println("Today is ", day) // Output: Today is 1
}
In this example, we define a custom-type Day
and use iota
to auto-increment the values of the constants. This approach is more concise and offers better type safety.
Optionally, we can also use like iota + 1
, if we want our constants to start from 1.
For this use case, we won’t need to define any variable or constant, we can simply inherit enum functionality using struct tags.
Here, I go with the Gin framework but the same can be done in any of the Go frameworks.
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
// Task represents a task with a status
type Task struct {
Status string `json:"status" binding:"oneof=active completed archived"`
}
func main() {
// Initialize Gin router
router := gin.Default()
// Define a route to create a task
router.POST("/tasks", createTask)
// Run the server
router.Run(":8080")
}
// Handler for creating a new task
func createTask(c *gin.Context) {
var task Task
// Bind the JSON request body to the Task struct
if err := c.BindJSON(&task); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Process the task (for demonstration purposes, just print the status)
fmt.Println("New task created with status:", task.Status)
// Respond with success message
c.JSON(http.StatusCreated, gin.H{"message": "Task created successfully"})
}
In the above snippet, We define a Task
struct with a Status
field. We use Gin's binding feature to specify that the Status
field must be one of the values active
, completed
, or archived
.
In case we invoke the API with any other values than of active
, completed
or archived
the API will return an error as we have told the Status
field to accept specific values only.
Try the below two test cases, and you will get it working.
{ "status" : "deleted"}
{ "status" : "active"}
package main
import (
"errors"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
// Status represents the status of a task
type Status string
const (
Active Status = "active"
Completed Status = "completed"
Archived Status = "archived"
)
// Task represents a task with a status
type Task struct {
Status Status `json:"status"`
}
// ValidateStatus validates the given task status
func ValidateStatus(status Status) error {
switch status {
case Active, Completed, Archived:
return nil
default:
return errors.New("invalid status")
}
}
// CreateTask handles the creation of a new task
func CreateTask(c *gin.Context) {
var task Task
// Bind the JSON request body to the Task struct
if err := c.BindJSON(&task); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Validate the task status
if err := ValidateStatus(task.Status); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Process the task (for demonstration purposes, just print the status)
fmt.Println("New task created with status:", task.Status)
// Respond with success message
c.JSON(http.StatusCreated, gin.H{"message": "Task created successfully"})
}
func main() {
// Initialize Gin router
router := gin.Default()
// Define a route to create a task
router.POST("/tasks", CreateTask)
// Run the server
router.Run(":8080")
}
ValidateStatus
function validates the given task status and returns an error if it's invalid. CreateTask
function, we call ValidateStatus
to check if the task status is valid before processing the task. If the status is invalid, the API will return a bad request response with an error message. Otherwise, we proceed to process the task as usual.Try the above two cases to test the behavior.
As a developer, we always want to make our system robust which doesn’t break even with unexpected inputs.
Enums enhance the robustness, flexibility, and usability of our Go applications. It ensures it will only allow the input values we have defined.
As you continue your journey with Go, adapt these techniques to suit your specific use cases and requirements.
Happy coding!
Get started today
Let's build the next
big thing!
Let's improve your business's digital strategy and implement robust mobile apps to achieve your business objectives. Schedule Your Free Consultation Now.
Get Free ConsultationGet started today
Let's build the next big thing!
Let's improve your business's digital strategy and implement robust mobile apps to achieve your business objectives. Schedule Your Free Consultation Now.
Get Free Consultation