goleak仅检测goroutine泄漏而非内存泄漏,需defer VerifyNone(t)在每个测试函数末尾显式调用,配合IgnoreCurrent()忽略手动启动的后台协程,并聚焦channel生命周期管理——90%泄漏源于channel未关闭或误操作。

goleak 不是内存泄露排查器,它只检测 goroutine 泄漏——而 goroutine 泄漏本身会导致内存间接上涨(比如协程持有了大对象),但它不检查 heap、allocs 或对象生命周期。别把它当 pprof heap 用,也别指望它报出“内存没释放”。
goleak.VerifyNone(t) 必须 defer,且必须在每个 TestXxx 函数里显式写
它不会自动生效,也不会继承到子测试。没 defer 就等于没加;放在函数中间或开头,panic 或提前 return 时就跳过了。
-
defer goleak.VerifyNone(t)要紧贴函数末尾写(哪怕只是占一行),确保所有路径(包括 panic、t.Fatal、t.Skip)都能触发检查 - 用了
t.Run()子测试?每个子测试函数体内都要单独加defer goleak.VerifyNone(t),父测试里的不传递 - 如果测试逻辑里启动了后台 goroutine(比如
go http.ListenAndServe(...)),不忽略就会被当成泄漏——得配合goleak.IgnoreCurrent()
手动启的 HTTP server、Ticker、client 都要显式 ignore
goleak 默认只忽略标准库内部启动的 goroutine(如 net/http 的监听协程),你代码里 go srv.Serve(lis) 或 time.NewTicker() 启的,它全算“新增泄漏”。
- 最稳妥的写法是:在子测试开头调用
defer goleak.IgnoreCurrent(),拍下当前 goroutine 快照,后续新增才纳入比对 - 避免用
goleak.IgnoreTopFunction("myapp.(*Server).Start")—— 编译器内联、栈帧抖动会让这个匹配失效,基本白配 - 第三方库(比如某 SDK 启了监控 goroutine)要忽略,得用完整包路径 + 函数名,例如
goleak.IgnoreTopFunction("github.com/some/pkg.startMonitor")
90% 的“泄漏”其实来自 channel 生命周期失控
goleak 报错堆栈常停在 runtime.selectgo 或 runtime.gopark,但根因几乎都是 channel 没关、或关得太早、或 nil channel 被 close。
立即学习“go语言免费学习笔记(深入)”;
- 谁创建
chan,谁负责close();向已close的 chan 发送会 panic,但接收还能读缓冲区 + 零值,容易让人误以为“还在工作” -
for range ch的 goroutine 必须等发送方close(ch)才退出;若发送方不确定结束时机,改用select { case - 别对
nilchannel 执行close(),也别重复close(),两者都直接 panic
CI 中跑 goleak 要设 timeout 并清理资源
goleak.VerifyNone(t) 本身不带超时,但测试里若有未终止的 goroutine(比如卡在 time.Sleep(1 * time.Hour)),整个 go test 会卡死,CI 流水线直接超时失败。
- 测试末尾加
time.Sleep(10 * time.Millisecond)不可靠——得确保待测逻辑真正结束,比如显式调用srv.Close()、ticker.Stop()、close(done) - CI 环境建议统一用
goleak.VerifyTestMain(m)包裹TestMain,但前提是所有后台 goroutine 都有明确 Stop 接口并被调用 - 线上或集成环境无法用 goleak?那就靠
pprof:启动时导入_ "net/http/pprof",访问http://localhost:6060/debug/pprof/goroutine?debug=2,重点看堆栈末尾卡在chan receive、semacquire、selectgo的长期 goroutine
真正难的不是加 goleak,而是让 goroutine 有明确的退出信号和关闭路径——channel、context、done channel、Stop 方法,缺一不可。goleak 只是那个提醒你“门没关好”的人,门怎么设计、锁怎么装,还得自己来。


















