Error Handling
In Part 2, I explore how error handling works in Go and how structured logging can be used to surface and trace failures effectively.
Go takes a fundamentally different approach to error handling. Without a
try/catch mechanism, errors are explicitly returned and handled at each step. While this may not seem substantial at first, it leads to more predictable and transparent control flow.
func (r *TaskRepository) GetAll(page int, pageSize int) []models.Task {
ctx := context.Background()
tasks, err := gorm.G[models.Task](r.Db).Limit(pageSize).Offset((page - 1) * pageSize).Order("created_at").Find(ctx)
if err != nil {
r.Log.Error("Failed to get all tasks" + err.Error())
return []models.Task{}
}
return tasks
}
Logging using slog
For logging, I used slog with a JSONHandler to produce structured logs. This format makes logs significantly easier to parse and analyze, especially in distributed or production environments. The logger is registered within the dependency container, allowing it to be injected across layers, including the repository. This ensures consistent logging while still capturing context-specific details at different points in the application.
func generateHandler() *slog.JSONHandler {
return slog.NewJSONHandler(os.Stderr, nil)
}
func CreateContainer() *dig.Container {
container := RegisterDb()
// Register json handler
err = container.Provide(generateHandler)
if err != nil {
panic(err)
}
return container
}
Consistent API Response
Another focus was on establishing a consistent API response model. Standardizing responses simplifies both testing and error handling by providing a predictable structure for all outcomes.
The API defines two response types: one for operations that return data and one for those that do not.
The ActionResult type includes a status code, an IsSuccess flag, and an optional error field. This error represents domain-level failures and provides a clear signal when a request does not succeed.
type ActionResult struct {
StatusCode int
IsSuccess bool
Error []string
}
ActionResultModel builds on this by introducing a generic Data field, allowing responses to carry typed payloads. This was a practical way to explore generics in Go, as well as underlying types to achieve composition. While Go is not an object-oriented programming language, it provides flexible mechanisms to model similar patterns without relying on inheritance.
type ActionResultModel[T any] struct {
Data T
ActionResult
}
Swagger documentation
While I’ve used Swagger before, implementing it in Go felt quite different. The Go community embraces a strong “build it yourself” mindset, which I appreciate—I enjoy that level of control, though not in every situation. Writing OpenAPI specifications for each endpoint was a great learning experience, but it’s easy to see how it could become overwhelming with a larger number of endpoints.
Next Up
Implementing request validation alongside authentication and authorization to ensure robust and secure API interactions.

Top comments (0)