协程泄漏本质是“何时暴露”而非“是否存在”,关键看 goroutine 数量趋势而非绝对值;需监控请求前后、压测后及长期运行的增量变化,并通过 debug=2 获取完整调用栈定位阻塞点。

协程泄漏不是“有没有”的问题,而是“什么时候暴露”的问题——只要没显式退出路径,它就在那里。
runtime.NumGoroutine() 怎么看才算真泄漏
数字高不等于泄漏。刚启动服务时跳到 80+ 很正常,那是 pprof、健康检查、日志采集等后台 goroutine 在初始化。关键看趋势,不是绝对值。
真正要盯的是这三种情形:
- 单次 HTTP 请求入口打点是 85,handler 返回前再查还是 85+,说明没回落
- 压测结束等待 30 秒后,总数仍比空闲态(比如 60)高出 40 以上
- 服务稳定运行几小时,从 50 → 180 → 420 这样单调爬升
实操建议:在关键路径加三处日志,别只采一次:log.Printf("goroutines@idle: %d", runtime.NumGoroutine())、@start、@done;采样间隔至少 time.Sleep(100 * time.Millisecond),否则漏掉靠超时退出的协程。
/debug/pprof/goroutine?debug=2 必须加 debug=2
?debug=1 只返回统计摘要,看不出谁卡在哪;?debug=2 才输出完整调用栈、阻塞时长和创建源头——这才是定位依据。
生产环境直接 curl http://localhost:6060/debug/pprof/goroutine?debug=2 就行。重点关注这些状态:
-
chan receive (nil chan):监听了未初始化或已关闭的 channel -
select卡死:所有 case 都不可达,且没写default -
semacquire:锁没释放、sync.WaitGroup忘记Done() -
IO wait:比如io.ReadFull卡住、ssh.Dial没设超时、http.Client没传context
如果看到几百个 goroutine 全卡在同一个函数里,阻塞时间显示 “432000s”(5 天),基本不用怀疑,就是它。
goleak.VerifyTestMain 为什么比 defer goleak.VerifyNone 更适合微服务测试
微服务测试常跨多个 package、启动全局后台协程(如健康检查 ticker、配置热更新),单个 TestXxx 函数里用 defer goleak.VerifyNone(t) 容易漏检或误报——它只看当前 test 结束时的 goroutine 快照,而真实泄漏可能在前一个 test 启动、后一个 test 才暴露。
正确做法是统一用 TestMain 包裹整个测试生命周期:
func TestMain(m *testing.M) {
os.Exit(goleak.VerifyTestMain(m))
}
这会在所有 test 执行完毕、所有 defer 清理后,再做一次终局检查。必须确保所有后台 goroutine(比如 http.Server、time.Ticker、第三方 client 的 monitor loop)都在 TestMain 返回前显式关闭或 stop。
忽略已知安全协程时,goleak.IgnoreTopFunction 的路径必须精确匹配运行时栈顶。错误写法:goleak.IgnoreTopFunction("health") —— goleak 不支持子串匹配,直接无效。正确流程是先跑一次失败 test,看报错堆栈第一行,例如:github.com/coreos/etcd/clientv3.(*watchGrpcStream).serveWatchClient,然后复制完整函数签名作为参数。
HTTP server 测试中 goroutine 泄漏的典型陷阱与修复
微服务几乎都带 HTTP 接口,而 http.Server 是泄漏重灾区:启动后不 Shutdown、Shutdown 未等完成、监听协程 panic 后没回收。
常见错误:
- 只调用
srv.Shutdown(ctx),但没wait它完成,导致监听 goroutine 还在跑 - 在 test 中用
goleak.IgnoreCurrent()忽略当前 goroutine,却忘了http.Server的监听协程是在ListenAndServe内部启动的,不在当前栈顶 - 测试中启了
http.Server,但没传context.WithTimeout或没 close listener,导致 goroutine 卡在Accept
修复要点:
- 显式调用
srv.Close()或srv.Shutdown(context.WithTimeout(...))并wait完成 - 若需忽略 server 监听协程,用
goleak.IgnoreTopFunction("net/http.(*Server).Serve"),注意包路径不能省 - 测试中避免直接
http.ListenAndServe,改用httptest.NewUnstartedServer或手动控制 listener 生命周期
最易被忽略的一点:goleak 不会自动识别你“本意是让它长期运行”,它只认快照前后是否多出未退出的 goroutine。哪怕你写了注释说“这是合法后台”,它也照报不误——所以过滤必须精确、主动、可验证。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











