← 返回文章列表

Go 并发模型:goroutine 泄漏排查实战

2026-06-12 · Go · Concurrency

Go 的并发原语写得爽,但线上 goroutine 泄漏排查起来是真折磨。

goroutine 泄漏的常见场景

  1. channel 无接收者:往 unbuffered channel 写但没人读
  2. channel 无发送者:range 遍历一个永远不会再写入的 channel
  3. context 未传递:子 goroutine 没收到 ctx.Done()
  4. time.After 在 select 中:每次 select 都创建新 timer,且不会被 GC

场景 1:生产者的 channel 没消费者

// 泄漏:resultChan 无人读取,worker 永远阻塞在 write
func brokenWorker(resultChan chan int) {
    for i := 0; i < 10; i++ {
        resultChan <- expensiveComputation(i)  // 第三个开始阻塞
    }
}

场景 2:time.After 囤积 timer

// 每次循环都创建新的 time.After,内部 timer 不会释放
for {
    select {
    case <-time.After(5 * time.Second):
        doWork()
    case <-ctx.Done():
        return
    }
}
// 修复:把 time.Ticker 提到循环外面
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
    select {
    case <-ticker.C:
        doWork()
    case <-ctx.Done():
        return
    }
}

排查工具

1. pprof goroutine profile

import _ "net/http/pprof"

// 查看当前所有 goroutine 堆栈
curl http://localhost:6060/debug/pprof/goroutine?debug=2

从堆栈里找"卡住"的位置——通常一搜 chan sendchan receive 就能定位。

2. runtime.NumGoroutine() 监控

在健康检查接口暴露 goroutine 数量,Prometheus + Grafana 画趋势图。数量只增不减就是泄漏。

3. goleak 测试

func TestNoLeak(t *testing.T) {
    defer goleak.VerifyNone(t)
    // 测试逻辑
}

经验

Go 的并发模型说"Don't communicate by sharing memory, share memory by communicating",但 channel 用不好就是泄漏的第一来源。现在我的规则是:每个 goroutine 必须有 ctx context.Context 参数,每个 channel 要么有 close 要么有 defer close