协程泄漏是goroutine未退出导致内存间接上涨,需先用runtime.numgoroutine()观察是否不回落,再通过/debug/pprof/goroutine?debug=2查卡在chan receive/select等状态的堆栈,配合goleak验证;而真正内存泄漏需确认heap_objects持续增长且存活对象不释放。

Go 语言里“协程泄漏”和“内存泄漏”常被混为一谈,但它们是两类不同问题:goroutine 泄漏不报错、不 panic,却会悄悄钉住大对象(比如 []byte、map、闭包捕获的结构体),导致 heap_inuse 持续上涨——你看到的“内存泄漏”,八成其实是 goroutine 没退出。
怎么快速确认是 goroutine 泄漏而不是内存分配过多
别急着开 pprof heap,先看 runtime.NumGoroutine() 的变化趋势:
- 单次请求处理完后,goroutine 数没回落(比如入口 85,handler 返回后仍是 85+)
- 压测结束后等 30 秒,总数仍比空闲态高一大截
- 稳定运行几小时,数字单调上升(50 → 180 → 420)
如果符合任一情形,且 /debug/pprof/goroutine?debug=2 中大量 goroutine 卡在 chan receive、select、io.ReadFull 或 net.Conn.Read,基本就是泄漏了。注意:?debug=1 只给统计摘要,看不出谁卡在哪;必须用 ?debug=2 才能看完整堆栈和阻塞时长——出现 432000s(5 天)这种量级,几乎 100% 是泄漏。
pprof /goroutineleak 端点在 Go 1.24+ 怎么启用
这是目前最接近“原生检测”的方式,但需手动触发 GC 才生效:
- 启动时加环境变量:
GODEBUG=goleak=1 - 代码中首次访问前调用
runtime.GC()(否则返回为空) - 访问
http://localhost:6060/debug/pprof/goroutineleak?debug=1
返回堆栈里带 _Gleaked 状态的 goroutine 就是确定泄露项。它只检测“阻塞在不可达同步原语上”的协程(比如向已关闭 channel 发送、所有 select case 都不可达),对死循环或无限重试无效。
goleak.VerifyNone 在测试里为什么总漏报
不是工具不行,是调用时机错了:
-
defer goleak.VerifyNone(t)放在 test 函数开头,但逻辑里启了个 goroutine 后立刻return,verify 执行时它可能还没跑完或刚结束 - 正确做法:确保待测逻辑完整跑完,比如显式
、<code>time.Sleep(10ms),再让 verify 检查 - 更稳的是用
goleak.VerifyTestMain(m)包裹整个TestMain,但得保证所有 goroutine(包括time.Ticker、后台 client)都在 test 结束前显式Stop()或close() - 常见误报源:
http.Serve、未 mock 的第三方 client、time.AfterFunc—— 这些要用goleak.IgnoreTopFunction过滤,比如goleak.IgnoreTopFunction("github.com/some/pkg.startMonitor")
哪些 channel 场景 100% 导致 goroutine 卡死
绝大多数泄漏都出在这几类写法上,只要出现,几乎必然泄漏:
- 向一个
nil chan发送或接收:var c chan int;c → 永久阻塞,<code>runtime不 panic,goroutine 直接挂住 - 从无缓冲 channel 读,但没人写;或向无缓冲 channel 写,但没人读
- 向有缓冲 channel 写满后继续写,且缓冲区从未被消费(例如
make(chan int, 1)写两次) -
select中所有case都不可达,且没有default:(比如所有 channel 都为nil或已关闭) -
for range ch死等,但忘记close(ch)
特别容易被忽略的是 http.Response.Body:不调 resp.Body.Close(),底层 persistConn 就不会释放,readLoop/writeLoop goroutine 就一直挂着。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











