mcpField
G
go-concurrency

go-concurrency

github.com/cxuu开发1861.3k 次安装19 周前更新
代码仓库

关于 go-concurrency

面向 Go 开发者的并发编程技能,涵盖 goroutine 生命周期、通道与同步原语的选择,以及并发安全文档等实践。

主要功能

  1. 01管理 goroutine 生命周期
  2. 02通道与互斥锁的选择
  3. 03通道方向与缓冲建议
  4. 04原子操作实践
  5. 05并发安全文档指引

适用场景

  1. 01设计可停止并等待的 goroutine
  2. 02选择共享状态的同步方式
  3. 03规划并发任务的取消与协作

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/cxuu/golang-skills --skill go-concurrency

安装后在 Claude Code 中输入/go-concurrency确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
ADVANCED-PATTERNS.md
BUFFER-POOLING.md
GOROUTINE-PATTERNS.md
SYNC-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(向已关闭的通道发送)、数据竞争、内存问题和资源泄漏。

核心规则
  1. 每个 goroutine 都需要停止机制——可预测的结束时间、取消信号,或两者兼备
  2. 代码必须能够等待 goroutine 结束
  3. 不要在 init() 中启动 goroutine——应提供生命周期方法(Close、Stop、Shutdown)
  4. 将同步范围限制在局部——限定在函数作用域内,并将逻辑拆分为同步函数
// 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 用户会认为只读操作可以安全地并发使用,而修改操作则不能。以下情况需要记录并发行为:

  1. 读操作与修改操作难以区分——例如,会修改 LRU 状态的 Lookup
  2. API 提供同步机制——例如,线程安全的客户端
  3. 接口有并发要求——在类型定义中记录

Context 使用

关于 context.Context 的使用指南(参数位置、结构体存储、自定义类型、派生模式),请参阅专门的 go-context 技能。


使用通道进行缓冲池化

使用带缓冲的通道作为空闲列表,以便重复使用已分配的缓冲区。这种“泄漏缓冲区”模式使用带 default 的 select 进行非阻塞操作。


相关技能

  • Context 传播:通过 goroutine 传递取消信号、截止时间或请求范围的值时,请参阅 go-context
  • 错误处理:从 goroutine 传播错误或使用 errgroup 时,请参阅 go-error-handling
  • 防御性加固:在 API 边界保护共享状态或使用 defer 进行清理时,请参阅 go-defensive
  • 接口设计:为包含 sync 原语的类型选择接收者类型时,请参阅 go-interfaces
外部资源

评论

支持 Markdown