goleak.verifynone(t) 必须用 defer 调用,否则 panic 或提前 return 时不会执行;需配合 goleak.ignorecurrent() 忽略测试中启动的 http server 等合法长期协程,且不可在 init() 中启动协程,否则会被误报为泄漏。

goleak.VerifyNone(t) 必须 defer 调用,否则测试失败时不执行
很多开发者把 goleak.VerifyNone(t) 直接写在测试函数末尾,结果一 panic 就跳过了,等于没加。它只在测试函数正常返回时才跑,而协程泄漏往往发生在异常路径里。
正确做法是:defer goleak.VerifyNone(t) —— 这样无论函数 return 还是 panic,都会触发检查。但要注意:如果测试里启动了长期运行的后台服务(比如 http.ListenAndServe),得显式忽略,否则会被误判:
-
defer goleak.VerifyNone(t, goleak.IgnoreCurrent()):忽略当前 goroutine 及其子 goroutine(适合测试中启的 HTTP server) - 别在
init()或包级变量初始化里启动 goroutine,VerifyNone会把它当泄漏报出来 - 它默认只比对测试执行期间新增的 goroutine,不扫描整个进程生命周期
协程未退出时,pprof 输出里常见阻塞状态有哪些
当 runtime.NumGoroutine() 显示数量持续上涨,下一步就得看 /debug/pprof/goroutine?debug=2 的输出。泄漏协程通常卡在这些状态:
-
chan receive:从 channel 读但没人发,或 channel 没 close -
chan send:往 channel 写但没人收,尤其无缓冲 channel -
IO wait:HTTP 请求没设超时、WebSocketWriteMessage卡住、TCP 连接未断开 -
select:select块里没监听ctx.Done(),导致永远等下去
重点不是看堆栈有多深,而是看同一调用链反复出现、且状态始终不变——那就是泄漏点。
context 传入 goroutine 后,必须用 select 监听 ctx.Done()
只把 context.Context 传进去没用,不监听就等于没带钥匙。常见错误是:
- 用
for range ch但没关 channel,协程永远等下一条消息 - 裸写
msg := ,没配 <code>select和超时,一卡到底 - 监听了
ctx.Done(),但没检查conn.WriteMessage()返回的io.EOF或websocket.ErrCloseSent
正确模式是:
go func(ctx context.Context, conn *websocket.Conn, ch <h3>测试里用 NumGoroutine() 做快速基线对比,别只看一次采样</h3> <p><code>runtime.NumGoroutine()</code> 是最轻量的第一道筛子,但它容易被系统 goroutine 波动干扰(GC、timer 等)。直接 sleep 100ms 后比一次数,很可能误报。</p> <p>稳妥做法是:</p>
- 操作前连续采样 3 次,取最小值作为基线
- 操作后同样采样 3 次,取最小值再减基线
- 差值 > 2 就要查——因为 Go 运行时本身可能临时多起 1~2 个 goroutine
- 配合
done chan struct{}或time.AfterFunc确认协程确实退出了,而不是“暂时没动”
真正难缠的是那种靠超时退出的协程,它不会立刻消失,得给足时间窗口再采样。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











