Golang 并发模式。在编写或审查涉及 goroutine、channel、select、锁、同步原语、errgroup、singleflight、工作池或扇出/扇入管道的并发 Go 代码时使用。当检测到 goroutine 泄漏、竞态条件、channel 所有权问题,或需要在 channel 和互斥锁之间做出选择时也会触发。
角色: 你是一名 Go 并发工程师。你假设每个 goroutine 在未被证明必要之前都是一种负担——正确性和无泄漏优先于性能。
模式:
- 编写模式 — 实现并发代码(goroutine、channel、同步原语、工作池、管道)。遵循下面的顺序指令。
- 审查模式 — 审查 PR 中的并发代码变更。关注差异:检查 goroutine 泄漏、缺少上下文传播、所有权违规以及未保护的共享状态。顺序执行。
- 审计模式 — 审计整个代码库中的现有并发代码。按照“并行化并发审计”部分所述,使用最多 5 个并行子代理。
社区默认。 明确覆盖
samber/cc-skills-golang@golang-concurrency技能的公司技能优先。
Go 并发最佳实践
Go 的并发模型基于 goroutine 和 channel。Goroutine 很廉价但并非免费——你创建的每个 goroutine 都是必须管理的资源。目标是结构化并发:每个 goroutine 都有明确的所有者、可预测的退出和正确的错误传播。
核心原则
- 每个 goroutine 必须有明确的退出方式 — 没有关闭机制(context、done channel、WaitGroup),它们会泄漏并累积直到进程崩溃
- 通过通信共享内存 — channel 显式传递所有权;互斥锁保护共享状态但使所有权隐式
- 在 channel 上发送副本,而非指针 — 发送指针会创建不可见的共享内存,违背了 channel 的目的
- 只有发送方关闭 channel — 从接收方关闭会导致发送方在关闭后写入时 panic
- 指定 channel 方向(
chan<-、<-chan)— 编译器在构建时防止误用 - 默认使用无缓冲 channel — 较大的缓冲区掩盖了背压;仅在经过测量的理由下使用
- 始终在 select 中包含
ctx.Done()— 没有它,goroutine 会在调用者取消后泄漏 - 避免在热循环中重复使用
time.After— 每次调用都会分配一个定时器并产生不必要的开销;对于长时间运行的循环,使用time.NewTimer+Reset - 在测试中使用
go.uber.org/goleak跟踪 goroutine 泄漏
有关详细的 channel/select 代码示例,请参阅 Channels and Select Patterns。
Channel 与互斥锁与原子操作
| 场景 | 使用 | 原因 |
|---|---|---|
| 在 goroutine 之间传递数据 | Channel | 传达所有权转移 |
| 协调 goroutine 生命周期 | Channel + context | 通过 select 实现干净关闭 |
| 保护共享结构体字段 | sync.Mutex / sync.RWMutex |
简单的临界区 |
| 简单计数器、标志 | sync/atomic |
无锁,开销更低 |
| 多读少写的 map | sync.Map |
针对读密集型工作负载优化。并发 map 读写会导致硬崩溃 |
| 缓存昂贵计算 | sync.Once / singleflight |
执行一次或去重 |
WaitGroup 与 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 |
并发 map,读密集型 | 无显式锁定;当写操作占主导时使用 RWMutex+map |
sync.Pool |
重用临时对象 | 始终在 Put() 前调用 Reset();减少 GC 压力 |
sync.Once |
一次性初始化 | Go 1.21+:OnceFunc、OnceValue、OnceValues |
sync.WaitGroup |
等待简单 goroutine | Go 1.25+:对于不会 panic 且不需要错误传播的即发即等任务,优先使用 wg.Go(func(){ ... })。对于 Go <1.25,使用 Add/Done。对于错误/取消/限制,使用带上下文的 errgroup。 |
x/sync/singleflight |
去重并发调用 | 缓存雪崩预防 |
x/sync/errgroup |
Goroutine 组 + 错误 | SetLimit(n) 替代手写工作池 |
有关详细示例和反模式,请参阅 Sync Primitives Deep Dive。
并发检查清单
在创建 goroutine 之前,回答以下问题:
- [ ] 它如何退出? — 上下文取消、channel 关闭或显式信号
- [ ] 我能通知它停止吗? — 传递
context.Context或 done channel - [ ] 我能等待它吗? —
sync.WaitGroup或errgroup - [ ] 谁拥有 channel? — 创建者/发送者拥有并关闭
- [ ] 这应该是同步的吗? — 不要在没有测量需求的情况下添加并发
管道和工作池
有关管道模式(扇出/扇入、有界工作器、生成器链、Go 1.23+ 迭代器、samber/ro),请参阅 Pipelines and Worker Pools。
并行化并发审计
在审计大型代码库中的并发时,使用最多 5 个并行子代理(Agent 工具):
- 查找所有 goroutine 创建(
go func、go method)并验证关闭机制 - 搜索没有同步的可变全局变量和共享状态
- 审计 channel 使用——所有权、方向、关闭、缓冲区大小
- 查找循环中的
time.After、select 中缺少的ctx.Done()、无限制的创建 - 检查互斥锁使用、
sync.Map、原子操作和线程安全文档
常见错误
| 错误 | 修复 |
|---|---|
| 即发即忘的 goroutine | 提供停止机制(context、done channel) |
| 从接收方关闭 channel | 只有发送方关闭 |
热循环中的 time.After |
重用 time.NewTimer + Reset |
select 中缺少 ctx.Done() |
始终在 context 上 select 以允许取消 |
| 无限制的 goroutine 创建 | 使用 errgroup.SetLimit(n) 或信号量 |
| 通过 channel 共享指针 | 发送副本或不可变值 |
在 goroutine 内部调用 wg.Add |
在 go 之前调用 Add——否则 Wait 可能提前返回 |
在 CI 中忘记 -race |
始终运行 go test -race ./... |
| 持有互斥锁进行 I/O | 保持临界区简短 |
交叉引用
- -> 参见
samber/cc-skills-golang@golang-performance技能,了解伪共享、缓存行填充、sync.Pool热路径模式 - -> 参见
samber/cc-skills-golang@golang-context技能,了解取消传播和超时模式 - -> 参见
samber/cc-skills-golang@golang-safety技能,了解并发 map 访问和竞态条件预防 - -> 参见
samber/cc-skills-golang@golang-troubleshooting技能,了解调试 goroutine 泄漏和死锁 - -> 参见
samber/cc-skills-golang@golang-design-patterns技能,了解优雅关闭模式 - -> 参见
samber/cc-skills-golang@golang-continuous-integration技能,了解使用这些指南在 CI 中进行自动化 AI 驱动的代码审查
Go 1.26 实验性 goroutine 泄漏分析
对于 Go 1.26 诊断,有一个实验性的 goroutine 泄漏分析。它对于面向生产的泄漏调查很有用,但需要通过 GOEXPERIMENT=goroutineleakprofile 启用;不要将其视为默认稳定行为。
启用实验时的典型用法:
curl http://localhost:6060/debug/pprof/goroutineleak?debug=2
go tool pprof http://localhost:6060/debug/pprof/goroutineleak
保留现有工具:
- 测试:
go.uber.org/goleak - 运行时计数:
runtime.NumGoroutine() - 堆栈转储:
/debug/pprof/goroutine?debug=2 - 竞态检查:
go test -race ./...






