mcpField
G
go-concurrency-review

go-concurrency-review

github.com/eduardo-sl开发83197 次安装2 周前更新
代码仓库

关于 go-concurrency-review

面向 Go 并发代码实现、审查和故障排查的技能,提供生命周期、同步、通道与上下文指引,并附有深入参考资料。

主要功能

  1. 01实现、差异审查与泄漏排查模式
  2. 02检查 Goroutine 生命周期和退出路径
  3. 03通道、互斥锁与原子操作指引
  4. 04检查上下文传递和共享状态
  5. 05要求使用竞态检测器测试

适用场景

  1. 01审查 Go 代码变更中的并发风险
  2. 02排查 Goroutine 泄漏或数据竞态
  3. 03审计通道所有权与关闭路径

安装

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

$npx skills add https://github.com/eduardo-sl/go-agent-skills --skill go-concurrency-review

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

包含的文件

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

references/
channels.md
goroutine-patterns.md
mutex-and-atomics.md

详细介绍

Go 并发审查

Go 并发功能强大,但很容易在不知不觉中出错。 这些模式可以防止 goroutine 泄漏、数据竞争和死锁。

详细参考资料,按需加载:

  • references/channels.md — 容量设置、信号通知、生产者关闭。
  • references/mutex-and-atomics.md — 互斥锁位置、锁的作用范围、原子操作、 sync.Once、RWMutex 常见问题、sync.Map、sync.Pool、singleflight。
  • references/goroutine-patterns.md — goroutine 边界处的 panic 恢复、流水线、有界扇出、定时器。

只有在下文内容不足时才阅读参考文件。

工作模式

开始前,选择与请求相符的模式:

  • 实现 — 编写新的并发代码。按照下方模式编写代码。
  • 差异审查(默认)— 根据每个部分检查变更的代码,特别留意新增的 go 语句和共享状态。
  • 泄漏/竞态排查 — 已观察到异常现象(goroutine 数量增长、-race 报告、死锁)。从“审查大型代码库”和竞态检测部分开始,定位问题。在 Go 1.27+ 中,先阅读 goroutineleak profile:它会列出阻塞在任何操作都无法解除的 goroutine。排查流程由 go-troubleshooting skill 负责。

审查大型代码库

进行完整的并发审查时,分别执行以下检查,而不是按顺序通读代码:

  1. Goroutine 生命周期: 找出每一条 go 语句 (grep -rn "go func\|go [a-zA-Z]" --include="*.go"),并确认每条语句都有终止路径(context、已关闭的 channel、WaitGroup)。
  2. 共享状态: 找出被多个 goroutine 访问的包级变量和结构体字段;确认它们受到互斥锁/原子操作保护。
  3. Channel 拓扑: 梳理每个 channel 的生产者/消费者;确认只关闭一次,且不存在向已关闭 channel 发送的路径。
  4. Context 传播: 确认阻塞调用接受并遵循 context.Context。

如果你的环境支持将工作委派给并行子 Agent 或任务,则为每项检查分配一个;否则按顺序执行。审查结果必须注明 file.go:line。最后务必运行 go test -race ./...。

选择原语

需求使用原因
在一个 goroutine 与另一个 goroutine 之间传递数据Channel发送操作会转移所有权
保护共享结构体的字段sync.Mutex / RWMutex临界区短,所有权明确
计数器或标志atomic.Int64、atomic.Bool无需锁,不会发生撕裂读取
计算一次,多次读取sync.OnceValue即使存在竞争也只计算一次
合并并发的相同调用singleflight.Group每个键只调用一次后端,而不是 N 次
在热点路径上复用短生命周期缓冲区sync.Pool减少分配——仅在分析 profile 后使用
等待 goroutine使用
结果不会出错sync.WaitGroup(wg.Go,Go 1.25+)
关注第一个错误errgroup.Group
第一个错误会取消其余任务errgroup.WithContext
限制同时运行的数量errgroup + SetLimit(n)——不要手动编写工作池

1. Goroutine 生命周期管理

每个 goroutine 都必须有明确的终止路径。禁止启动后不管。

使用 errgroup 协调 goroutine:
g, ctx := errgroup.WithContext(ctx)

g.Go(func() error {
    return fetchUsers(ctx)
})

g.Go(func() error {
    return fetchOrders(ctx)
})

if err := g.Wait(); err != nil {
    return fmt.Errorf("fetch data: %w", err)
}
长时间运行的 goroutine 必须遵循 context:
func (w *Worker) Run(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case job := <-w.jobs:
            if err := w.process(job); err != nil {
                w.logger.Error("process job", slog.Any("error", err))
            }
        }
    }
}
在所有者处启动 goroutine,而不是在被调用方处:
// ✅ Good — caller controls lifecycle
go worker.Run(ctx)

// ❌ Bad — function secretly starts goroutine
func NewWorker() *Worker {
    w := &Worker{}
    go w.run() // hidden goroutine — caller has no control
    return w
}
在 goroutine 边界处恢复 panic

任何 goroutine 中的 panic 都会终止整个进程。HTTP 中间件中的 recover 只覆盖请求 goroutine,不覆盖它启动的 goroutine。运行依赖输入或第三方代码的工作线程和后台任务,应在自身的延迟函数中恢复 panic、记录堆栈,并将 panic 转换为错误。不要通过恢复来掩盖自己代码中的 bug——应在崩溃会连带影响无关工作的地方进行恢复。 示例:references/goroutine-patterns.md。

2. 通道模式

  • 缓冲大小为 0 或 1。无缓冲通道是同步点;缓冲大小为 1 则用于交接。任何更大的缓冲区都需要注释说明选择该大小的理由——随意写个 100,等到生产环境比预发布环境更慢时就会成为一个 bug。

  • 信号通道使用 struct{},而 close(done) 会同时向所有接收方广播。

  • 由生产者拥有通道,并且只有生产者负责关闭通道,在生产 goroutine 中使用 defer close(ch)。每次发送都必须放在与 ctx.Done() 对比的 select 中,否则消费者一旦退出,就会导致生产者泄漏。

  • 流水线:每个阶段关闭自己的输出;使用 errgroup.SetLimit 限制扇出;单 goroutine 链应使用 iter.Seq,而不是通道。

示例见 references/channels.md 和 references/goroutine-patterns.md。

3. 互斥锁和原子操作

  • 零值 sync.Mutex 和 sync.RWMutex 可直接使用。*sync.Mutex 字段总是不对的。
  • 将互斥锁直接声明在其保护的字段上方,并用注释说明它们之间的关系。声称互斥锁保护“整个结构体”,却没有明确保护对象,等于什么也没保护。
  • 临界区应尽可能小——持锁时绝不要调用外部服务,也不要再获取第二把锁。
  • 🔴 绝不要复制包含互斥锁的值(c2 := *c1):副本会带有原锁的状态。go vet 能捕获其中大多数问题;相信它。
  • 计数器和标志应使用 sync/atomic 类型,而不是用互斥锁保护 int64。
  • 对必须且仅需执行一次的延迟初始化,使用 sync.Once。
  • RWMutex 不能升级锁,也不能递归读锁;sync.Map 只适用于两类工作负载;sync.Pool 中的对象必须重置并限制大小;singleflight 使用第一个调用者的上下文——应将其分离。详见 references/mutex-and-atomics.md。

示例见 references/mutex-and-atomics.md。

4. 上下文传播

上下文始终是第一个参数,绝不能是结构体字段;每个阻塞操作都必须监听 ctx.Done():

// ✅ Good
select {
case result := <-ch:
    return result, nil
case <-ctx.Done():
    return nil, ctx.Err()
}

// ❌ Bad — blocks forever if the context is cancelled
result := <-ch

每次外部调用都应派生一个带有独立超时的子上下文,并立即执行 defer cancel()。完整规则见 go-context 技能。

5. 避免可变全局变量

// ❌ Bad — mutable global, not safe for concurrent access
var db *sql.DB

// ✅ Good — pass as dependency
type Server struct {
    db *sql.DB
}

竞态检测

CI 期间始终使用竞态检测器运行测试:

go test -race ./...

这一点不容妥协。测试套件在没有 -race 的情况下通过,无法证明并发正确性。

危险信号检查清单

  • 🔴 启动 goroutine 时没有关闭路径
  • 🔴 通道从未关闭(可能导致 goroutine 泄漏)
  • 🔴 按值复制互斥锁
  • 🔴 将上下文存储在结构体字段中
  • 🔴 有可用的父上下文,却使用 context.Background()
  • 🔴 阻塞操作中的 select 缺少 ctx.Done() 分支
  • 🔴 共享的 map/slice 未经同步就被访问
  • 🟡 缓冲通道的大小随意设得很大
  • 🟡 使用 time.Sleep 进行同步,而不是使用恰当的信号通知
  • 🟡 在 init() 或构造函数中启动 goroutine,却没有生命周期控制
  • 🟡 后台 goroutine 运行依赖输入的代码,却没有 recover

评论

支持 Markdown