Persona: You are a Go concurrency engineer. You assume every goroutine is a liability until proven necessary β correctness and leak-freedom come before performance.
Modes:
- Write mode β implement concurrent code (goroutines, channels, sync primitives, worker pools, pipelines). Follow the sequential instructions below.
- Review mode β reviewing a PR's concurrent code changes. Focus on the diff: check for goroutine leaks, missing context propagation, ownership violations, and unprotected shared state. Sequential.
- Audit mode β auditing existing concurrent code across a codebase. Use up to 5 parallel sub-agents as described in the "Parallelizing Concurrency Audits" section.
Community default. A company skill that explicitly supersedes samber/cc-skills-golang@golang-concurrency skill takes precedence.
Go Concurrency Best Practices
Go's concurrency model is built on goroutines and channels. Goroutines are cheap but not free β every goroutine you spawn is a resource you must manage. The goal is structured concurrency: every goroutine has a clear owner, a predictable exit, and proper error propagation.
Core Principles
- Every goroutine must have a clear exit β without a shutdown mechanism (context, done channel, WaitGroup), they leak and accumulate until the process crashes
- Share memory by communicating β channels transfer ownership explicitly; mutexes protect shared state but make ownership implicit
- Send copies, not pointers on channels β sending pointers creates invisible shared memory, defeating the purpose of channels
- Only the sender closes a channel β closing from the receiver side panics if the sender writes after close
- Specify channel direction (
chan<-, <-chan) β the compiler prevents misuse at build time
- Default to unbuffered channels β larger buffers mask backpressure; use them only with measured justification
- Always include
ctx.Done() in select β without it, goroutines leak after caller cancellation
- Never use
time.After in loops β each call creates a timer that lives until it fires, accumulating memory. Use time.NewTimer + Reset
- Track goroutine leaks in tests with
go.uber.org/goleak
For detailed channel/select code examples, see Channels and Select Patterns.
Channel vs Mutex vs Atomic
| Scenario |
Use |
Why |
| Passing data between goroutines |
Channel |
Communicates ownership transfer |
| Coordinating goroutine lifecycle |
Channel + context |
Clean shutdown with select |
| Protecting shared struct fields |
sync.Mutex / sync.RWMutex |
Simple critical sections |
| Simple counters, flags |
sync/atomic |
Lock-free, lower overhead |
| Many readers, few writers on a map |
sync.Map |
Optimized for read-heavy workloads. Concurrent map read/write causes a hard crash |
| Caching expensive computations |
sync.Once / singleflight |
Execute once or deduplicate |
WaitGroup vs errgroup
| Need |
Use |
Why |
| Wait for goroutines, errors not needed |
sync.WaitGroup |
Fire-and-forget |
| Wait + collect first error |
errgroup.Group |
Error propagation |
| Wait + cancel siblings on first error |
errgroup.WithContext |
Context cancellation on error |
| Wait + limit concurrency |
errgroup.SetLimit(n) |
Built-in worker pool |
Sync Primitives Quick Reference
| Primitive |
Use case |
Key notes |
sync.Mutex |
Protect shared state |
Keep critical sections short; never hold across I/O |
sync.RWMutex |
Many readers, few writers |
Never upgrade RLock to Lock (deadlock) |
sync/atomic |
Simple counters, flags |
Prefer typed atomics (Go 1.19+): atomic.Int64, atomic.Bool |
sync.Map |
Concurrent map, read-heavy |
No explicit locking; use RWMutex+map when writes dominate |
sync.Pool |
Reuse temporary objects |
Always Reset() before Put(); reduces GC pressure |
sync.Once |
One-time initialization |
Go 1.21+: OnceFunc, OnceValue, OnceValues |
sync.WaitGroup |
Wait for goroutine completion |
Add before go; Go 1.24+: wg.Go() simplifies usage |
x/sync/singleflight |
Deduplicate concurrent calls |
Cache stampede prevention |
x/sync/errgroup |
Goroutine group + errors |
SetLimit(n) replaces hand-rolled worker pools |
For detailed examples and anti-patterns, see Sync Primitives Deep Dive.
Concurrency Checklist
Before spawning a goroutine, answer:
Pipelines and Worker Pools
For pipeline patterns (fan-out/fan-in, bounded workers, generator chains, Go 1.23+ iterators, samber/ro), see Pipelines and Worker Pools.
Parallelizing Concurrency Audits
When auditing concurrency across a large codebase, use up to 5 parallel sub-agents (Agent tool):
- Find all goroutine spawns (
go func, go method) and verify shutdown mechanisms
- Search for mutable globals and shared state without synchronization
- Audit channel usage β ownership, direction, closure, buffer sizes
- Find
time.After in loops, missing ctx.Done() in select, unbounded spawning
- Check mutex usage,
sync.Map, atomics, and thread-safety documentation
Common Mistakes
| Mistake |
Fix |
| Fire-and-forget goroutine |
Provide stop mechanism (context, done channel) |
| Closing channel from receiver |
Only the sender closes |
time.After in hot loop |
Reuse time.NewTimer + Reset |
Missing ctx.Done() in select |
Always select on context to allow cancellation |
| Unbounded goroutine spawning |
Use errgroup.SetLimit(n) or semaphore |
| Sharing pointer via channel |
Send copies or immutable values |
wg.Add inside goroutine |
Call Add before go β Wait may return early otherwise |
Forgetting -race in CI |
Always run go test -race ./... |
| Mutex held across I/O |
Keep critical sections short |
Cross-References
- -> See
samber/cc-skills-golang@golang-performance skill for false sharing, cache-line padding, sync.Pool hot-path patterns
- -> See
samber/cc-skills-golang@golang-context skill for cancellation propagation and timeout patterns
- -> See
samber/cc-skills-golang@golang-safety skill for concurrent map access and race condition prevention
- -> See
samber/cc-skills-golang@golang-troubleshooting skill for debugging goroutine leaks and deadlocks
- -> See
samber/cc-skills-golang@golang-design-patterns skill for graceful shutdown patterns
References