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
- 1. 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 + INLINECODE6 - Track goroutine leaks in tests with INLINECODE7
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 | INLINECODE14 | 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 |
|---|
| INLINECODE18 | Protect shared state | Keep critical sections short; never hold across I/O |
| INLINECODE19 |
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:
- - [ ] How will it exit? — context cancellation, channel close, or explicit signal
- [ ] Can I signal it to stop? — pass
context.Context or done channel - [ ] Can I wait for it? —
sync.WaitGroup or INLINECODE41 - [ ] Who owns the channels? — creator/sender owns and closes
- [ ] Should this be synchronous instead? — don't add concurrency without measured need
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):
- 1. 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
角色: 你是一名 Go 并发工程师。你假定每个 goroutine 在未被证明必要之前都是一种负担——正确性和无泄漏比性能更重要。
模式:
- - 编写模式 —— 实现并发代码(goroutine、通道、同步原语、工作池、管道)。遵循以下顺序指令。
- 审查模式 —— 审查 PR 中的并发代码变更。聚焦于差异:检查 goroutine 泄漏、缺少上下文传播、所有权违规以及未受保护的共享状态。顺序执行。
- 审计模式 —— 审计代码库中现有的并发代码。按照“并行化并发审计”部分所述,最多使用 5 个并行子代理。
社区默认。 明确取代 samber/cc-skills-golang@golang-concurrency 技能的公司技能具有优先权。
Go 并发最佳实践
Go 的并发模型建立在 goroutine 和通道之上。Goroutine 很廉价,但并非免费——你创建的每个 goroutine 都是你必须管理的资源。目标是结构化并发:每个 goroutine 都有明确的所有者、可预测的退出方式以及正确的错误传播。
核心原则
- 1. 每个 goroutine 必须有明确的退出方式 —— 没有关闭机制(上下文、done 通道、WaitGroup),它们会泄漏并累积,直到进程崩溃
- 通过通信来共享内存 —— 通道显式地转移所有权;互斥锁保护共享状态,但使所有权变得隐式
- 在通道上发送副本,而不是指针 —— 发送指针会创建不可见的共享内存,违背了通道的初衷
- 只有发送者才能关闭通道 —— 如果发送者在关闭后写入,从接收端关闭会导致 panic
- 指定通道方向(chan<-、<-chan)—— 编译器在构建时防止误用
- 默认使用无缓冲通道 —— 较大的缓冲区会掩盖背压;仅在经过测量的理由下使用它们
- 始终在 select 中包含 ctx.Done() —— 没有它,goroutine 会在调用者取消后泄漏
- 切勿在循环中使用 time.After —— 每次调用都会创建一个定时器,该定时器会一直存在直到触发,从而累积内存。请使用 time.NewTimer + Reset
- 在测试中使用 go.uber.org/goleak 跟踪 goroutine 泄漏
有关详细的通道/select 代码示例,请参阅通道和 Select 模式。
通道 vs 互斥锁 vs 原子操作
| 场景 | 使用 | 原因 |
|---|
| 在 goroutine 之间传递数据 | 通道 | 传达所有权转移 |
| 协调 goroutine 生命周期 |
通道 + 上下文 | 使用 select 进行干净关闭 |
| 保护共享结构体字段 | sync.Mutex / sync.RWMutex | 简单的临界区 |
| 简单的计数器、标志 | sync/atomic | 无锁,开销更低 |
| 映射上的多读少写 | sync.Map | 针对读密集型工作负载进行了优化。
并发映射读写会导致硬崩溃 |
| 缓存昂贵的计算 | sync.Once / singleflight | 执行一次或去重 |
WaitGroup vs errgroup
| 需求 | 使用 | 原因 |
|---|
| 等待 goroutine,不需要错误 | sync.WaitGroup | 即发即弃 |
| 等待 + 收集第一个错误 |
errgroup.Group | 错误传播 |
| 等待 + 在第一个错误时取消兄弟任务 | errgroup.WithContext | 出错时取消上下文 |
| 等待 + 限制并发 | errgroup.SetLimit(n) | 内置工作池 |
同步原语快速参考
| 原语 | 使用场景 | 关键说明 |
|---|
| sync.Mutex | 保护共享状态 | 保持临界区简短;切勿在 I/O 期间持有 |
| sync.RWMutex |
多读少写 | 切勿将 RLock 升级为 Lock(死锁) |
| sync/atomic | 简单的计数器、标志 | 优先使用类型化原子操作(Go 1.19+):atomic.Int64、atomic.Bool |
| sync.Map | 并发映射,读密集型 | 无显式锁定;当写入占主导时使用 RWMutex+map |
| sync.Pool | 重用临时对象 | 始终在 Put() 之前调用 Reset();减少 GC 压力 |
| sync.Once | 一次性初始化 | Go 1.21+:OnceFunc、OnceValue、OnceValues |
| sync.WaitGroup | 等待 goroutine 完成 | 在 go 之前调用 Add;Go 1.24+:wg.Go() 简化了用法 |
| x/sync/singleflight | 去重并发调用 | 防止缓存雪崩 |
| x/sync/errgroup | Goroutine 组 + 错误 | SetLimit(n) 替代了手写的工作池 |
有关详细示例和反模式,请参阅同步原语深入探讨。
并发检查清单
在创建 goroutine 之前,请回答:
- - [ ] 它将如何退出? —— 上下文取消、通道关闭或显式信号
- [ ] 我能向它发出停止信号吗? —— 传递 context.Context 或 done 通道
- [ ] 我能等待它吗? —— sync.WaitGroup 或 errgroup
- [ ] 谁拥有这些通道? —— 创建者/发送者拥有并关闭
- [ ] 这应该是同步的吗? —— 未经测量需要,不要添加并发
管道和工作池
有关管道模式(扇出/扇入、有界工作者、生成器链、Go 1.23+ 迭代器、samber/ro),请参阅管道和工作池。
并行化并发审计
在审计大型代码库的并发性时,最多使用 5 个并行子代理(Agent 工具):
- 1. 查找所有 goroutine 创建(go func、go method)并验证关闭机制
- 搜索没有同步的可变全局变量和共享状态
- 审计通道使用——所有权、方向、关闭、缓冲区大小
- 查找循环中的 time.After、select 中缺少的 ctx.Done()、无限制的创建
- 检查互斥锁使用、sync.Map、原子操作和线程安全文档
常见错误
| 错误 | 修复 |
|---|
| 即发即弃的 goroutine | 提供停止机制(上下文、done 通道) |
| 从接收者关闭通道 |
只有发送者才能关闭 |
| 热循环中的 time.After | 重用 time.NewTimer + Reset |
| select 中缺少 ctx.Done() | 始终在 select 中包含上下文以允许取消 |
| 无限制的 goroutine 创建 | 使用 errgroup.SetLimit(n) 或信号量 |
| 通过通道共享指针 | 发送副本或不可变值 |
| 在 goroutine 内部调用 wg.Add | 在 go 之前调用 Add——否则 Wait 可能提前返回 |
| 在 CI 中忘记 -race | 始终运行 go test -race ./... |
| 在 I/O 期间持有互斥锁 | 保持临界区简短 |
交叉引用
- - -> 有关伪共享、缓存行填充、sync.Pool 热路径模式,请参阅 samber/cc-skills-golang@golang-performance 技能
- -> 有关取消传播和超时模式,请参阅 samber/cc-skills-golang@golang-context 技能
- -> 有关并发映射访问和竞态条件预防,请参阅 samber/cc-skills-golang@golang-safety 技能
- -> 有关调试 goroutine 泄漏和死锁,请参阅 samber/cc-skills-golang@golang-troubleshooting 技能
- -> 有关优雅关闭模式,请参阅 samber/cc-skills-golang@golang-design-patterns 技能
参考