DEV Community

Cover image for Exploring Go through backend engineering practices (Part 2)
Adexandria
Adexandria

Posted on

Exploring Go through backend engineering practices (Part 2)

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

}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.

Swagger documentation of the API


Next Up

Implementing request validation alongside authentication and authorization to ensure robust and secure API interactions.

Top comments (0)