关于 go-concurrency
面向 Go 开发者的并发编程技能,涵盖 goroutine 生命周期、通道与同步原语的选择,以及并发安全文档等实践。
主要功能
- 01管理 goroutine 生命周期
- 02通道与互斥锁的选择
- 03通道方向与缓冲建议
- 04原子操作实践
- 05并发安全文档指引
适用场景
- 01设计可停止并等待的 goroutine
- 02选择共享状态的同步方式
- 03规划并发任务的取消与协作
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ cxuu/ golang-skills --skill go-concurrency安装后在 Claude Code 中输入/go-concurrency确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/ADVANCED-PATTERNS.mdBUFFER-POOLING.mdGOROUTINE-PATTERNS.mdSYNC-PRIMITIVES.md详细介绍
Go 并发
兼容性:Atomic 示例可在适用时使用标准库的类型化原子操作;如果项目已依赖
go.uber.org/atomic,也可使用它。
资源路由
references/GOROUTINE-PATTERNS.md- 在启动、停止或等待 goroutine 时阅读。references/SYNC-PRIMITIVES.md- 在选择互斥锁、原子操作、通道和类似 once 的原语时阅读。references/BUFFER-POOLING.md- 在考虑基于通道或类似 sync.Pool 的复用方式时阅读。references/ADVANCED-PATTERNS.md- 在使用工作池、流水线、errgroup 和大量涉及取消的模式时阅读。
Goroutine 生命周期
规范性:启动 goroutine 时,必须明确它们何时或是否会退出。
Goroutine 可能因阻塞在通道发送或接收操作上而泄漏。即使没有其他 goroutine 持有对通道的引用,GC 也不会终止被阻塞的 goroutine。即使在运行中的 goroutine 不会泄漏,也会导致 panic(向已关闭的通道发送)、数据竞争、内存问题和资源泄漏。
核心规则
- 每个 goroutine 都需要停止机制——可预测的结束时间、取消信号,或两者兼备
- 代码必须能够等待 goroutine 结束
- 不要在
init()中启动 goroutine——应提供生命周期方法(Close、Stop、Shutdown) - 将同步范围限制在局部——限定在函数作用域内,并将逻辑拆分为同步函数
// Good: Clear lifetime with WaitGroup.Go (Go 1.25+)
var wg sync.WaitGroup
for item := range queue {
item := item
wg.Go(func() { process(ctx, item) })
}
wg.Wait()
// Bad: No way to stop or wait
go func() { for { flush(); time.Sleep(delay) } }()
使用 go.uber.org/goleak 测试泄漏。
原则:启动 goroutine 前,务必明确它将如何停止。
通过通信共享
“不要通过共享内存来通信;应通过通信来共享内存。”
这是 Go 并发设计的基础原则。使用通道进行所有权转移和编排——当一个 goroutine 生成值,另一个 goroutine 消费值时使用通道。如果多个 goroutine 访问共享状态,而使用通道会带来不必要的复杂性,则使用互斥锁。
默认使用通道。 当问题本质上是保护共享数据结构(例如缓存或计数器),而不是在 goroutine 之间传递数据时,再使用 sync.Mutex / sync.RWMutex。
同步函数
规范性:优先使用同步函数,而非异步函数。
| 优势 | 原因 |
|---|---|
| goroutine 局部化 | 更容易推理其生命周期 |
| 避免泄漏和竞争 | 更容易防止资源泄漏和数据竞争 |
| 更易测试 | 无需轮询即可检查输入/输出 |
| 调用方更灵活 | 调用方可在需要时添加并发 |
建议性:调用方很难(有时甚至不可能)移除不必要的并发。让调用方在需要时再添加并发。
零值互斥锁
sync.Mutex 和 sync.RWMutex 的零值有效——几乎不需要使用指向互斥锁的指针。
// Good: Zero-value is valid // Bad: Unnecessary pointer
var mu sync.Mutex mu := new(sync.Mutex)
不要嵌入互斥锁——应使用名为 mu 的字段,将 Lock/Unlock 保留为实现细节,而非导出的 API。
通道方向
规范性:尽可能指定通道方向。
通道方向可以防止错误(编译器会捕获关闭只读通道的操作)、传达所有权,并让代码更易于理解。
func produce(out chan<- int) { /* send-only */ }
func consume(in <-chan int) { /* receive-only */ }
func transform(in <-chan int, out chan<- int) { /* both */ }
通道容量:零或一
通道容量应为 零(无缓冲)或 一。其他任何容量都需要说明:
- 容量是如何确定的
- 如何防止通道在负载下被填满
- 写入方阻塞时会发生什么
c := make(chan int) // unbuffered — Good
c := make(chan int, 1) // size one — Good
c := make(chan int, 64) // arbitrary — needs justification
原子操作
使用 atomic.Bool、atomic.Int64 等(Go 1.19 起的标准库 sync/atomic,或 go.uber.org/atomic)进行类型安全的原子操作。使用原始 int32/int64 字段时,很容易在某些代码路径中忘记使用原子操作访问。
// Good: Type-safe // Bad: Easy to forget
var running atomic.Bool var running int32 // atomic
running.Store(true) atomic.StoreInt32(&running, 1)
running.Load() running == 1 // race!
并发文档说明
提示:如果无法从操作类型明显判断线程安全性,请记录线程安全相关信息。
Go 用户会认为只读操作可以安全地并发使用,而修改操作则不能。以下情况需要记录并发行为:
- 读操作与修改操作难以区分——例如,会修改 LRU 状态的
Lookup - API 提供同步机制——例如,线程安全的客户端
- 接口有并发要求——在类型定义中记录
Context 使用
关于 context.Context 的使用指南(参数位置、结构体存储、自定义类型、派生模式),请参阅专门的 go-context 技能。
使用通道进行缓冲池化
使用带缓冲的通道作为空闲列表,以便重复使用已分配的缓冲区。这种“泄漏缓冲区”模式使用带 default 的 select 进行非阻塞操作。
相关技能
- Context 传播:通过 goroutine 传递取消信号、截止时间或请求范围的值时,请参阅 go-context
- 错误处理:从 goroutine 传播错误或使用 errgroup 时,请参阅 go-error-handling
- 防御性加固:在 API 边界保护共享状态或使用 defer 进行清理时,请参阅 go-defensive
- 接口设计:为包含 sync 原语的类型选择接收者类型时,请参阅 go-interfaces
外部资源
- 启动 goroutine 前,务必明确它将如何 停止 — Dave Cheney
- 重新思考经典并发 模式 — Bryan Mills (GopherCon 2018)
- Go 程序何时结束 — Go Time 播客
- go.uber.org/goleak — 用于测试的 goroutine 泄漏检测器
- go.uber.org/atomic — 类型安全的 原子操作