Go Concurrency
Goroutines, channels, context, and errgroup for Go 1.26 — plus the number-one
documented pitfall: leaking goroutines on an unbuffered channel + early return.
Agent Workflow (MANDATORY)
Before ANY implementation, spawn 3 agents in parallel, one Agent call each with a name:
- fuse-ai-pilot:explore-codebase - Map existing goroutine/channel/context usage
- fuse-ai-pilot:research-expert - Verify errgroup/context docs via Context7/Exa
- mcp__context7__query-docs - Confirm
golang.org/x/sync/errgroup signatures
After implementation, run fuse-ai-pilot:sniper for validation, and run tests
with go test -race ./....
Overview
| Feature |
Description |
| Goroutines & channels |
Lightweight concurrency + typed communication |
| errgroup |
Parallelism + error aggregation + context cancellation |
| context |
First param, propagated strictly, carries cancellation/deadline |
| WaitGroup vs channels |
Counting-only vs result/error passing |
| Race detector |
-race in tests/CI to catch data races |
| Leak profile (1.26) |
GOEXPERIMENT=goroutineleakprofile / /debug/pprof/goroutineleak |
Critical Rules
context.Context is the first parameter - named ctx, never stored in a struct
- Every started goroutine must be able to exit - or it leaks (see rule 4)
- Prefer
errgroup for fan-out with errors - it handles wait + first error + cancel
- Unbuffered channel + early return = leak - senders block forever; buffer or drain
- Test with
-race - a passing test without -race proves nothing about races
Architecture
internal/
├── fetch/
│ ├── fetch.go # errgroup.WithContext fan-out, bounded by SetLimit
│ └── worker.go # worker pool: fixed goroutines drain a jobs channel
└── pipeline/
└── stage.go # ctx-cancellable stages, buffered hand-off channels
→ See errgroup-patterns.md for full example
Reference Guide
Concepts
| Topic |
Reference |
When to Consult |
| Goroutines & channels |
goroutines-channels.md |
Buffered vs not, select, WaitGroup vs channels |
| errgroup |
errgroup.md |
Fan-out, error aggregation, SetLimit, TryGo |
| context |
context-propagation.md |
Cancellation, deadlines, propagation rules |
| Goroutine leaks |
goroutine-leaks.md |
The #1 pitfall + the 1.26 leak profile |
Templates
| Template |
When to Use |
| errgroup-patterns.md |
Bounded parallel work with error handling |
| worker-pool.md |
Fixed workers draining a job queue |
Quick Reference
Parallel work with errgroup
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(8) // bound concurrency
for _, u := range urls {
g.Go(func() error { return fetch(ctx, u) })
}
if err := g.Wait(); err != nil { // first non-nil error; cancels ctx
return err
}
→ See errgroup.md
Avoid the leak (buffer so senders never block)
ch := make(chan result, len(items)) // buffered → early return can't strand senders
→ See goroutine-leaks.md
Best Practices
DO
- Pass
ctx first and thread it through every blocking call
- Reach for
errgroup before hand-rolling WaitGroup + error channels
- Buffer result channels to the number of senders, or fully drain them
- Run
go test -race; try GOEXPERIMENT=goroutineleakprofile in CI (1.26)
DON'T
- Return early from a fan-out while goroutines still block on an unbuffered channel
- Store a
context.Context in a struct field
- Use a bare
sync.WaitGroup when goroutines return errors (use errgroup)
- Assume tests are race-free without the
-race flag
1---2name: go-concurrency3description: Use when writing or reviewing Go concurrency — goroutines, channels, errgroup, context cancellation, or goroutine leaks. Not for sequential idioms (go-core-idioms).4---56<objective>7Covers Go concurrency for Go 1.26: goroutines and channels, golang.org/x/sync/errgroup,8context propagation and cancellation, sync.WaitGroup vs channels, the -race detector,9and diagnosing goroutine leaks (including the 1.26 goroutineleak profile). Does not10cover sequential error handling, slog, generics, or interface style (see11go-core-idioms), non-Go languages, or framework-specific code.12</objective>1314# Go Concurrency1516Goroutines, channels, `context`, and `errgroup` for Go 1.26 — plus the number-one17documented pitfall: **leaking goroutines on an unbuffered channel + early return.**1819## Agent Workflow (MANDATORY)2021Before ANY implementation, spawn 3 agents in parallel, one `Agent` call each with a `name`:22231. **fuse-ai-pilot:explore-codebase** - Map existing goroutine/channel/context usage242. **fuse-ai-pilot:research-expert** - Verify errgroup/context docs via Context7/Exa253. **mcp__context7__query-docs** - Confirm `golang.org/x/sync/errgroup` signatures2627After implementation, run **fuse-ai-pilot:sniper** for validation, and run tests28with `go test -race ./...`.2930---3132## Overview3334| Feature | Description |35|---------|-------------|36| **Goroutines & channels** | Lightweight concurrency + typed communication |37| **errgroup** | Parallelism + error aggregation + context cancellation |38| **context** | First param, propagated strictly, carries cancellation/deadline |39| **WaitGroup vs channels** | Counting-only vs result/error passing |40| **Race detector** | `-race` in tests/CI to catch data races |41| **Leak profile (1.26)** | `GOEXPERIMENT=goroutineleakprofile` / `/debug/pprof/goroutineleak` |4243---4445## Critical Rules46471. **`context.Context` is the first parameter** - named `ctx`, never stored in a struct482. **Every started goroutine must be able to exit** - or it leaks (see rule 4)493. **Prefer `errgroup` for fan-out with errors** - it handles wait + first error + cancel504. **Unbuffered channel + early return = leak** - senders block forever; buffer or drain515. **Test with `-race`** - a passing test without `-race` proves nothing about races5253---5455## Architecture5657```58internal/59├── fetch/60│ ├── fetch.go # errgroup.WithContext fan-out, bounded by SetLimit61│ └── worker.go # worker pool: fixed goroutines drain a jobs channel62└── pipeline/63 └── stage.go # ctx-cancellable stages, buffered hand-off channels64```6566→ See [errgroup-patterns.md](references/templates/errgroup-patterns.md) for full example6768---6970## Reference Guide7172### Concepts7374| Topic | Reference | When to Consult |75|-------|-----------|-----------------|76| **Goroutines & channels** | [goroutines-channels.md](references/goroutines-channels.md) | Buffered vs not, select, WaitGroup vs channels |77| **errgroup** | [errgroup.md](references/errgroup.md) | Fan-out, error aggregation, SetLimit, TryGo |78| **context** | [context-propagation.md](references/context-propagation.md) | Cancellation, deadlines, propagation rules |79| **Goroutine leaks** | [goroutine-leaks.md](references/goroutine-leaks.md) | The #1 pitfall + the 1.26 leak profile |8081### Templates8283| Template | When to Use |84|----------|-------------|85| [errgroup-patterns.md](references/templates/errgroup-patterns.md) | Bounded parallel work with error handling |86| [worker-pool.md](references/templates/worker-pool.md) | Fixed workers draining a job queue |8788---8990## Quick Reference9192### Parallel work with errgroup9394```go95g, ctx := errgroup.WithContext(ctx)96g.SetLimit(8) // bound concurrency97for _, u := range urls {98 g.Go(func() error { return fetch(ctx, u) })99}100if err := g.Wait(); err != nil { // first non-nil error; cancels ctx101 return err102}103```104105→ See [errgroup.md](references/errgroup.md)106107### Avoid the leak (buffer so senders never block)108109```go110ch := make(chan result, len(items)) // buffered → early return can't strand senders111```112113→ See [goroutine-leaks.md](references/goroutine-leaks.md)114115---116117## Best Practices118119### DO120- Pass `ctx` first and thread it through every blocking call121- Reach for `errgroup` before hand-rolling `WaitGroup` + error channels122- Buffer result channels to the number of senders, or fully drain them123- Run `go test -race`; try `GOEXPERIMENT=goroutineleakprofile` in CI (1.26)124125### DON'T126- Return early from a fan-out while goroutines still block on an unbuffered channel127- Store a `context.Context` in a struct field128- Use a bare `sync.WaitGroup` when goroutines return errors (use `errgroup`)129- Assume tests are race-free without the `-race` flag