Error Handling and ErrGroup Patterns in Go
The article explains Go’s built‑in error interface, distinguishes errors, exceptions and panics, presents three handling patterns—classic returns, stateful objects, and functional deferred execution—shows how to wrap errors for context, and demonstrates using the errgroup concurrency primitive (including an extended version) for safe parallel processing.
In Go, error handling is a fundamental part of building reliable services. The language defines a built‑in error interface with a single method Error() string , which serves as the standard way to represent recoverable errors.
Unlike exceptions in other languages, Go distinguishes three concepts:
Error : an expected problem such as a failed database connection, which should be handled explicitly by the caller.
Exception : an unexpected condition (e.g., nil‑pointer dereference) that triggers a panic .
panic : used for unrecoverable situations; production code should avoid calling panic except during startup failures.
The article outlines three common patterns for handling errors in Go:
1. Classic Go logic (returning error )
type ZooTour interface {
Enter() error
VisitPanda(panda *Panda) error
Leave() error
}
func Tour(t ZooTour, panda *Panda) error {
if err := t.Enter(); err != nil {
return errors.WithMessage(err, "Enter failed.")
}
if err := t.VisitPanda(panda); err != nil {
return errors.WithMessage(err, "VisitPanda failed.")
}
// ...
return nil
}2. Storing error inside an object (stateful handling)
type ZooTour interface {
Enter() error
VisitPanda(panda *Panda) error
Leave() error
Err() error
}
func Tour(t ZooTour, panda *Panda) error {
t.Enter()
t.VisitPanda(panda)
t.Leave()
if err := t.Err(); err != nil {
return errors.WithMessage(err, "ZooTour failed")
}
return nil
}3. Functional programming style (deferred execution)
type Walker interface { Next MyFunc }
type SliceWalker struct {
index int
funs []MyFunc
}
func NewEnterFunc() MyFunc {
return func(t ZooTour) error { return t.Enter() }
}
func BreakOnError(t ZooTour, walker Walker) error {
for {
f := walker.Next()
if f == nil { break }
if err := f(t); err != nil { return err }
}
return nil
}These patterns can be chosen based on the complexity of the business logic: simple sequential checks, stateful pipelines, or more abstract functional flows.
For richer error information, Go developers often use error‑wrapping libraries such as github.com/pkg/errors . The library provides functions like New , WithMessage , WithStack , Wrapf , and Cause to attach stack traces and additional context.
// Create a new error with stack trace
err := errors.New("something went wrong")
// Add context to an existing error
err = errors.WithMessage(err, "while processing request")
// Retrieve the root cause
root := errors.Cause(err)In layered architectures (DAO → Service → Controller), a typical error‑handling flow looks like:
// controller
if err := mode.ParamCheck(param); err != nil {
log.Errorf("param=%+v", param)
return errs.ErrInvalidParam
}
return mode.ListTestName("")
// service
_, err := dao.GetTestName(ctx, settleId)
if err != nil {
log.Errorf("GetTestName failed. err: %v", err)
return errs.ErrDatabase
}
// dao
if err != nil {
log.Errorf("GetTestDao failed. query: %s error(%v)", sql, err)
return err
}Key take‑aways for error handling include:
Log every error with appropriate severity; use %+v to print stack traces.
Wrap errors at the point of propagation to preserve context.
Prefer error handling over panic for business‑level failures.
The article also introduces errgroup , a concurrency primitive from golang.org/x/sync/errgroup , which simplifies running multiple goroutines and collecting the first error.
type Group struct {}
func (g *Group) WithContext(ctx context.Context) (*Group, context.Context)
func (g *Group) Go(f func() error)
func (g *Group) Wait() errorTypical usage:
func TestErrgroup() {
eg, ctx := errgroup.WithContext(context.Background())
for i := 0; i < 100; i++ {
i := i // capture loop variable
eg.Go(func() error {
time.Sleep(2 * time.Second)
select {
case <-ctx.Done():
fmt.Println("Canceled:", i)
return nil
default:
fmt.Println("End:", i)
return nil
}
})
}
if err := eg.Wait(); err != nil {
log.Fatal(err)
}
}Bilibili’s extended errgroup adds features such as concurrency limiting, panic recovery, and a task queue. Its core structure includes a channel of functions, a slice for overflow tasks, and a context with cancel function.
type Group struct {
err error
wg sync.WaitGroup
errOnce sync.Once
workerOnce sync.Once
ch chan func(ctx context.Context) error
chs []func(ctx context.Context) error
ctx context.Context
cancel func()
}
func WithContext(ctx context.Context) *Group { return &Group{ctx: ctx} }
func (g *Group) Go(f func(ctx context.Context) error) {
g.wg.Add(1)
if g.ch != nil {
select { case g.ch <- f: default: g.chs = append(g.chs, f) }
return
}
go g.do(f)
}When using the extended version, developers should be aware of potential deadlocks if the task slice grows without proper synchronization, and that the internal WaitGroup only tracks the producer side, not the consumer pool.
Overall, the article emphasizes that robust error handling in Go involves clear distinction between recoverable errors and unrecoverable panics, systematic error wrapping to retain context, and using concurrency primitives like errgroup to manage parallel work safely.
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
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.