有效数字高不等于泄漏,刚启动时跳到100+属正常初始化;真正需盯趋势:请求后不回落、压测后30秒不恢复、长期单调上升(如50→180→420);实操须三处日志打点、配合done chan或time.afterfunc确认退出、采样3次取最小值作baseline。

runtime.NumGoroutine() 怎么看才算有效
数字高 ≠ 泄漏,刚启动时 runtime.NumGoroutine() 跳到 100+ 很正常——那是 pprof、健康检查、日志采集等后台协程在初始化。真正要盯的是趋势:单次请求入口打点是 85,handler 返回前再查还是 85+;压测结束等 30 秒,总数仍比空闲态(比如 60)高出 40 以上;服务跑几小时从 50 → 180 → 420 单调爬升。
实操建议:
- 在关键路径加三处日志:log.Printf("goroutines@idle: %d", runtime.NumGoroutine())、@start、@done
- 采样间隔不能只靠 time.Sleep(100 * time.Millisecond) ——有些协程靠超时退出,得配合 done chan struct{} 或 time.AfterFunc 显式确认退出时机
- 系统 goroutine 本身有波动,采样 3 次取最小值作 baseline 更稳
/debug/pprof/goroutine?debug=2 怎么用才不白查
默认 ?debug=1 只给统计摘要,看不出谁卡在哪;必须加 ?debug=2 才输出完整调用栈和阻塞时长。生产环境直接 curl http://localhost:6060/debug/pprof/goroutine?debug=2 就行。
重点关注这些状态的 goroutine:
- chan receive (nil chan):监听了未初始化或已关闭的 channel
- select 卡死:所有 case 都不可达,且没写 default 或没响应 ctx.Done()
- semacquire:锁没释放、sync.WaitGroup 忘记 Done()
- IO wait:比如 io.ReadFull 卡住、http.Client 没配 context、ssh.Dial 没设超时
- 如果几百个 goroutine 全卡在同一个函数里,阻塞时间显示 “432000s”(5 天),基本不用怀疑,就是它
goleak.VerifyNone(t) 为什么总报错却找不到源头
不是工具不行,是调用时机或忽略逻辑错了:
- 必须写成 defer goleak.VerifyNone(t),否则 test panic 时不会执行
- 别把 defer 放在 test 函数开头就 return 的逻辑前面——待测 goroutine 可能根本没跑完
- 合法后台服务(如 net/http.(*Server).Serve)必须显式忽略:goleak.IgnoreTopFunction("net/http.(*Server).Serve"),漏掉包名就白配
- 若测试里启了 time.Ticker,得先 ticker.Stop() 再让 VerifyNone 检查,否则它会当成泄漏报出来
pprof goroutineleak 端点在 Go 1.24+ 怎么启用
这是目前最接近“原生检测”的方式,但需手动触发 GC 才生效:
- 启动时加环境变量:GODEBUG=goleak=1
- 代码中首次访问前调用 runtime.GC()(否则返回为空)
- 访问 http://localhost:6060/debug/pprof/goroutineleak?debug=1
- 返回堆栈里带 _Gleaked 状态的 goroutine 就是确定泄露项
注意:它只检测“阻塞在不可达同步原语上”的协程(比如向已关闭 channel 发送、所有 select case 都不可达),对死循环或无限重试无效。真正难缠的泄漏,往往藏在 context 传递缺失、第三方库未 Close、resp.Body 未读完就丢弃这类细节里。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











