Mastering Go Backend: Project Structure, Error Handling, and Observability Best Practices

This article explores practical Go backend development techniques, covering project organization, package naming, internal packages, init usage, layer separation (controller, service, dao), dependency injection, global variable pitfalls, observability with logging, tracing and monitoring, comprehensive error handling, and DAO layer automation.

Go Development Architecture Practice
Go Development Architecture Practice
Go Development Architecture Practice
Mastering Go Backend: Project Structure, Error Handling, and Observability Best Practices

1. Project Structure

Organizing a Go project starts with a clear directory layout; there is no fixed rule, but package names should be concise yet expressive, and comments should explain purpose, especially for abbreviated names like mdw (middleware).

1.1 Keep package names simple and meaningful

Use common short names such as fmt, strconv, pkg, cmd, but prefer clarity over brevity; add comments to describe the package’s role.

1.2 Use internal packages

Placing code in an internal directory forces developers to think about what should be public versus private, improving modularity.

1.3 Avoid careless use of init

Code in init runs before the main program and can introduce hidden side‑effects, especially in large projects with many dependencies; prefer explicit constructors like NewXX() or InitXX().

1.4 Be cautious with generic util / common packages

Instead of vague names, create purpose‑specific packages (e.g., time_helper); if a util package becomes widely used, refactor it early to avoid future entanglement.

2. Code Structure

2.1 Layer separation (c/s/d)

Typical layers are:

Controller (or handler) : entry point, parameter parsing, response formatting; keep thin and avoid business logic.

Service : contains core business logic; should not directly embed SQL queries.

DAO (repository) : abstracts data access, hides database specifics, often wraps an ORM like GORM.

2.2 Dependency passing

Controllers depend on services, services on DAOs. Avoid constructing lower‑level objects inside higher‑level constructors; instead use global variables sparingly or employ a dependency‑injection framework such as wire:

var XX *XXService = &XXService{}

type XXService struct {}

func (x *XXService) XX() {}

Or with constructor injection:

type XXService struct { xRepo XXRepo }

func NewXXService(r *XXRepo) *XXService { }

Wire can generate the wiring code:

// wire.Build(repo.NewGoodsRepo, svc.NewGoodsSvc, controller.NewGoodsController)
// wire framework auto‑generates
func initControllers() (*Controllers, error) {
    goodsRepo := repo.NewGoodsRepo()
    goodsSvc := svc.NewGoodsSvc(goodsRepo)
    goodsController := controller.NewGoodsController(goodsSvc)
    return goodsController, nil
}

2.3 Minimize global variables

Global loggers or DB handles lead to uncontrolled access and noisy logs; prefer creating instances (e.g., zap.New()) and passing them explicitly.

3. Observability

Observability combines logging, tracing, and monitoring. Use Prometheus for metrics, Jaeger for distributed tracing, and structured logs that include trace IDs. Instrumentation should be added early in the design phase.

3.1 Tracing DB/Redis/Log calls

If a library supports context, pass it to capture spans; otherwise wrap the library (e.g., a Redis client) to inject tracing manually.

type Repo interface {
    Set(ctx context.Context, key, value string, ttl time.Duration, options ...Option) error
    Get(ctx context.Context, key string, options ...Option) (string, error)
    // ... other methods
}

type cacheRepo struct { client *redis.Client }

func (c *cacheRepo) Get(ctx context.Context, key string, options ...Option) (string, error) {
    var err error
    ts := time.Now()
    opt := newOption()
    defer func() {
        if opt.TraceRedis != nil {
            opt.TraceRedis.Timestamp = time_parse.CSTLayoutString()
            opt.TraceRedis.Handle = "get"
            opt.TraceRedis.Key = key
            opt.TraceRedis.CostSeconds = time.Since(ts).Seconds()
            opt.TraceRedis.Err = err
            addTracing(ctx, opt.TraceRedis)
        }
    }()
    for _, f := range options { f(opt) }
    value, err := c.client.Get(ctx, key).Result()
    if err != nil { err = werror.Wrapf(err, "redis get key: %s err", key) }
    return value, err
}

4. Error Handling

4.1 Response error design

Define a unified error type that carries HTTP status, business code, message, and the original error. Example helper functions:

func NewError(httpCode, businessCode int, msg string) Error {}
func NewErrorWithStatusOk(businessCode int, msg string) Error {}
func NewErrorWithStatusOkAutoMsg(businessCode int) Error {}
func NewErrorAutoMsg(httpCode, businessCode int) Error {}
func (e *err) WithErr(err error) Error {}

Usage in a service method:

func (s *GoodsSvc) AddGoods(sctx core.SvcContext, param *model.GoodsAdd) error {
    // ...
    if err != nil {
        return response.NewErrorAutoMsg(http.StatusInternalServerError, response.ServerError).WithErr(err)
    }
}

4.2 Go error handling nuances

Since Go 1.13, fmt.Errorf("%w", err) supports error wrapping. For stack traces, third‑party packages like github.com/pkg/errors are used. To aggregate multiple errors without aborting, github.com/hashicorp/go-multierror can collect them.

5. DAO Layer Practices

5.1 Code generation

Tools such as gormt can generate CRUD code for GORM, reducing boilerplate.

5.2 Hiding field names

Expose only needed fields via helper functions (e.g., FindBy(id, dao.Columns.ID, dao.Columns.Name)) and keep database column names out of business logic.

5.3 Updating fields safely

Because Go’s zero values are indistinguishable from omitted fields, use pointer fields in request structs to differentiate “not provided” from “set to zero”. Example:

type UpdateScore struct {
    Id         int
    Name       *string
    Score      *int
    CreateTime *string
}

Only non‑nil fields are updated, avoiding ambiguity between zero and missing values.

6. References

https://github.com/HYY-yu/seckill.shop

https://github.com/xinliangnote/go-gin-api

https://goframe.org/pages/viewpage.action?pageId=3672891

https://go-kratos.dev/docs/guide/wire

https://goframe.org/pages/viewpage.action?pageId=3673684

backendobservabilityerror-handlingdependency-injectionproject-structure
Go Development Architecture Practice
Written by

Go Development Architecture Practice

Daily sharing of Golang-related technical articles, practical resources, language news, tutorials, real-world projects, and more. Looking forward to growing together. Let's go!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.