---
trigger: model_decision
description: Apply when Go code uses goroutines, channels, mutexes, worker pools, or background jobs.
---

# Go concurrency

- Every goroutine has an owner and an exit path. Before writing `go func()`, know what stops it (context cancellation, closed channel, or finished work) and who waits for it.
- Use `errgroup.WithContext` for "run N things, fail fast"; `g.SetLimit(n)` for bounded parallelism. Use `sync.WaitGroup` (`wg.Go(...)`) when errors aren't returned.
- Pass `ctx` into goroutines and `select` on `ctx.Done()` in loops and blocking sends.
- Channels: the sender closes, never the receiver. Unbuffered by default; buffer only with a reason (known producer burst). Don't use channels where a mutex is simpler.
- Protect shared state with `sync.Mutex` held for short sections (and never across I/O); `sync/atomic` types (`atomic.Int64`) for counters.
- Don't copy structs containing mutexes; pass pointers.
- Timers: use `time.NewTimer`/`time.After` inside `select` with ctx; stop tickers with `defer t.Stop()`.
- Don't start goroutines in HTTP handlers that outlive the request unless they're handed to a managed background worker.
- Recover panics in long-lived worker goroutines and report them; a panic in any goroutine crashes the program.
- Test concurrent code with `go test -race`, and use `testing/synctest` for code that depends on time.

## Smells to fix
Unbounded `go` in loops over input, `time.Sleep` for synchronization, ignored errors from goroutines, leaked goroutines waiting on channels nobody closes.
