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.
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
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!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
