← 返回文章列表
Go 并发模型:goroutine 泄漏排查实战
Go 的并发原语写得爽,但线上 goroutine 泄漏排查起来是真折磨。
goroutine 泄漏的常见场景
- channel 无接收者:往 unbuffered channel 写但没人读
- channel 无发送者:range 遍历一个永远不会再写入的 channel
- context 未传递:子 goroutine 没收到 ctx.Done()
- 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 send 或 chan 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。