errgroup & golang.org/x/sync Extensions
Coordinated goroutines with first-error cancellation.
Search across all documentation pages
Coordinated goroutines with first-error cancellation.
golang.org/x/sync/errgroup groups goroutines that share fate: one failure cancels the rest and surfaces a single error from Wait.
Related packages in x/sync add bounded parallelism (SetLimit), deduplication (singleflight), and counting semaphores (semaphore).
These extensions fill gaps in the stdlib sync package for service-style orchestration.
Quick-reference recipe card - copy-paste ready.
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(4)
g.Go(func() error { return stepA(ctx) })
g.Go(func() error { return stepB(ctx) })
if err := g.Wait(); err != nil {
return err
}When to reach for this:
SetLimit).singleflight.Group.semaphore.Weighted.// go.mod: module example.com/errgroupdemo
package main
import (
"context"
"fmt"
"sync"
"time"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
"golang.org/x/sync/singleflight"
)
func fetch(ctx context.Context, name string, delay time.Duration, fail bool) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay):
if fail {
return fmt.Errorf("%s failed", name)
}
fmt.Println("fetched", name)
return nil
}
}
func parallelFetch(ctx context.Context) error {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(2)
tasks := []struct {
name string
d time.Duration
fail bool
}{
{"users", 20 * time.Millisecond, false},
{"orders", 30 * time.Millisecond, false},
{"inventory", 10 * time.Millisecond, true},
}
for _, t := range tasks {
t := t
g.Go(func() error {
return fetch(ctx, t.name, t.d, t.fail)
})
}
return g.Wait()
}
func dedupeLoad(g *singleflight.Group, key string) (string, error) {
v, err, _ := g.Do(key, func() (any, error) {
time.Sleep(50 * time.Millisecond)
return "data-for-" + key, nil
})
if err != nil {
return "", err
}
return v.(string), nil
}
func main() {
ctx := context.Background()
if err := parallelFetch(ctx); err != nil {
fmt.Println("parallel:", err)
}
var sf singleflight.Group
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
s, _ := dedupeLoad(&sf, "account:42")
fmt.Println(s)
}()
}
wg.Wait()
sem := semaphore.NewWeighted(1)
if err := sem.Acquire(ctx, 1); err == nil {
defer sem.Release(1)
fmt.Println("critical section")
}
}What this demonstrates:
WithContext ties goroutine lifetime to shared cancel.SetLimit(2) prevents more than two fetches at once.singleflight runs one loader per key while waiters share the result.semaphore.Weighted blocks with context awareness unlike raw channel tricks.errgroup.Group embeds a WaitGroup plus an error slot protected by mutex.Go triggers cancel on the derived context from WithContext.SetLimit(n) uses an internal channel semaphore; n < 1 means unlimited.singleflight.Do coalesces concurrent calls with the same key until the leader fn returns.| Package | Type | Role |
|---|---|---|
| errgroup | Group | Parallel tasks, first error wins |
| semaphore | Weighted | Acquire/release capacity with ctx |
| singleflight | Group | Suppress duplicate in-flight work |
| syncmap | Map | Concurrent map (niche; often prefer plain map+mutex) |
// errgroup without cancel - use when failures should not stop siblings
var g errgroup.Group
g.Go(func() error { return maybeFail() })
_ = g.Wait()errgroup.Group when partial success is acceptable and you merge errors yourself.WithContext into I/O, not the parent.Go after sibling failed - Work continues after cancel. Fix: Use derived ctx only.Go after Wait - Undefined behavior; create a new Group per batch. Fix: One group per logical operation.ctx.Done() in fetch bodies.Go - Crashes process; errgroup does not recover. Fix: Keep panics out; use safe wrappers in production.sync.WaitGroup for simple join.| Alternative | Use When | Don't Use When |
|---|---|---|
sync.WaitGroup + err chan | Custom error merge logic | Standard first-error-cancel is enough |
parallel.For (Go 1.22+ iter) | CPU slices with sync errors | Mixed I/O with cancel |
| Worker pool channel | Long-lived queue | One-shot parallel batch |
Manual context.WithCancel | Fine-grained partial cancel | Boilerplate without benefit |
Automatic error return and optional context cancel on first failure.
When any failure should stop sibling goroutines - typical for request handlers fanning out.
Unlimited parallelism - same as not calling SetLimit.
Use a mutex slice, or run tasks sequentially, or third-party multi-error types.
errgroup intentionally returns one.
No - it only dedupes concurrent loads.
Pair with an actual cache for stored results.
Only within one process.
Use distributed locks or cache for cross-instance dedupe.
Start a slow task and a failing fast task; assert slow returns context.Canceled.
Semaphore supports weighted acquire, context cancel, and clearer API for dynamic limits.
Often yes for concurrent send/recv helpers tied to stream lifecycle.
It is an official Go extension module versioned separately from stdlib.
Pin in go.mod and verify at CI time.
Derive child WithContext from parent ctx so outer cancel still applies.
Goroutines still run but leaks possible if they block forever - always Wait.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026