go-concurrencylisted
Install: claude install-skill Markuysa/agent-skills
# Go concurrency
Every goroutine needs an owner who knows when it stops. Concurrency bugs in
production are almost never exotic — they are goroutines nobody stops, contexts
nobody passes, and errors nobody reads.
## Goroutine lifecycle
Before writing `go func()`, answer three questions:
1. **Who stops it?** A context, a closed channel, or a returning loop.
2. **Who reads its error?** An `errgroup`, an error channel, or it panics the process.
3. **Who waits for it?** Shutdown must not exit while it's mid-write.
If any answer is "nobody", you are writing a leak.
```go
// bad — unowned, unstoppable, error vanishes
go processAll(items)
// good — bounded, cancellable, errors surface
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(runtime.GOMAXPROCS(0))
for _, item := range items {
g.Go(func() error {
return process(ctx, item)
})
}
if err := g.Wait(); err != nil {
return fmt.Errorf("process items: %w", err)
}
```
`errgroup.WithContext` cancels the derived context on the first error, so siblings
stop instead of finishing work whose result is already discarded. Use `SetLimit`
to bound concurrency — unbounded fan-out over a slice of unknown size is how you
exhaust connection pools.
## Context
- Pass `ctx` as the **first parameter**, always named `ctx`. Never store it in a
struct field (the one accepted exception is a request-scoped struct that lives
and dies with the request).
- Only the function that creates a cancel must call it: `defer cancel()`, al