Go 1.24+ 可用 /debug/pprof/goroutineleak 端点检测 goroutine 泄漏,需启用 GODEBUG=goleak=1,首次访问前应调用 runtime.GC() 确保泄漏可被识别。
Go 服务内存持续上涨、CPU 调度变慢、pprof 显示成千上万 goroutine 卡在
或
上?基本可以断定是 goroutine 泄漏 —— 它不报错,但比内存泄漏更难察觉、更早压垮服务。
怎么用 pprof 快速确认 goroutine 是否在泄露
pprof 的
是第一道筛子,但它默认只显示正在运行或阻塞的 goroutine,对“已卡死但未被 GC 回收”的泄露 goroutine 不敏感。真正有用的是 Go 1.24+ 实验性支持的
端点(需启用
):
启动时加环境变量:
访问
,返回的堆栈里带
状态的 goroutine 就是确定泄露项
注意:该端点仅在 GC 完成一次“泄漏感知循环”后才可用,首次请求可能为空,建议触发一次手动 GC:
后再查
旧版本 Go(/debug/pprof/goroutine?debug=2 + 人工比对生命周期
goleak 在单元测试里为什么没报出来
很容易漏掉真实泄漏,不是工具不行,而是使用姿势错了:
它只捕获测试函数退出时“还活着”的 goroutine,如果泄漏 goroutine 在测试结束前就因 channel 关闭/超时退出了,就不会被捕获
常见误用:在
之后又启动 goroutine 并立即
,导致 verify 执行时 goroutine 已结束
正确做法:确保待测逻辑完整跑完(比如加
或等某个 done chan),再让 verify 检查
更稳的方式是用
包裹整个
,覆盖所有测试用例的 goroutine 生命周期
哪些 channel 场景 100% 导致 goroutine 卡死
绝大多数 goroutine 泄漏都出在 channel 使用上,以下模式只要出现,几乎必然泄漏:
go语言参考手册 中文CHM版
Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易。本文给大家带来Go参考手册,需要的可以来下载! Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。 Go 语言特色 简洁、快速、安全 并行、有趣、开源 内存管理、v数组安全、编译
下载
立即学习
“
go语言免费学习笔记(深入)
”;
向一个
发送或接收:
不会 panic,goroutine 直接挂住
从无缓冲 channel 读,但没人写;或向无缓冲 channel 写,但没人读
向有缓冲 channel 写满后继续写,且缓冲区从未被消费(比如
写两次)
中所有 case 都不可达,且没有
:例如所有 channel 都为
或已关闭,就会卡死
忘记 close(channel),却在另一端用
死等
ssh.Dial 或 http.Client 导致的 goroutine 堆积怎么定位
这类泄漏往往不显眼,但线上最常见 —— 表现为大量 goroutine 卡在
、
、
或
:
排查重点不是业务代码,而是连接生命周期管理:是否每次
后都调用了
?是否复用了
但没控制并发数?
HTTP 场景下,检查
的
和
是否过小或为 0,导致连接池耗尽后新建连接不断堆积 goroutine
用
查看句柄数是否逼近上限,间接验证连接泄漏
对
,务必确保每个
调用
或
,否则底层 readLoop goroutine 不会退出
goroutine 泄漏最难的不是发现,而是确认“它到底算不算泄漏”—— 同一个阻塞状态,在调试阶段可能是正常等待,在高负载下却成了泄漏起点。所以别只看堆栈,要结合超时控制、连接池配置、channel 生命周期三者交叉验证。一旦怀疑,优先加
包裹所有阻塞操作,这是最简单也最有效的兜底手段。
chan receiveselect/debug/pprof/goroutine?debug=2/debug/pprof/goroutineleakGODEBUG=gctrace=1,goleak=1GODEBUG=goleak=1 go run main.gohttp://localhost:6060/debug/pprof/goroutineleak?debug=1_Gleakedruntime.GC()goleak.VerifyNone(t)defer goleak.VerifyNone(t)returntime.Sleep(10ms)goleak.VerifyTestMainTestMainnil chanvar c chan int; → 永久阻塞,runtimemake(chan int, 1)selectdefaultnilfor range channelio.ReadFullreadLoopwriteLoopnet.Conn.Readssh.Dialclient.Close()*ssh.Clienthttp.TransportMaxIdleConnsMaxIdleConnsPerHostlsof -p $PID | grep "can't identify protocol"golang.org/x/crypto/ssh*ssh.Sessionsession.Wait()session.Close()context.WithTimeout