排查 Go 服务的 goroutine 泄漏,以前是件体力活:线上 pprof 一把抓出几千个 goroutine,然后逐个翻堆栈,靠肉眼判断哪个是永久阻塞的“钉子户”。
Go 1.27 带来了一个专门解决这个问题的 profile 类型:goroutineleak。它不报告所有 goroutine,只报告永久阻塞(leaked)的那部分。实现机制上,它借助 GC 的可达性分析——GC 标记阶段会扫描对象与协程栈的引用关系,如果一个 goroutine 在 GC 眼中已不可达,同时又处于永久等待状态,那大概率就是泄漏。这个思路把“人工逐行看堆栈”变成了“运行时帮你筛一遍”。
启用方式
这个特性最早在 Go 1.26 作为实验特性引入,构建时需要显式打开实验开关:
GOEXPERIMENT=goroutineleakprofile go build -o app ./cmd/server
同时,runtime/pprof 包中加入了对应支持,net/http/pprof 会暴露 /debug/pprof/goroutineleak 端点。
到了 Go 1.27,goroutineleak 转正为 GA,不再需要 GOEXPERIMENT,编译器、运行时默认支持。Web 服务只需照常引入:
import _ "net/http/pprof"
然后访问:
GET /debug/pprof/goroutineleak
或者用 go tool pprof 拉取并交互式查看:
go tool pprof http://localhost:6060/debug/pprof/goroutineleak
返回的结果直接列出泄漏 goroutine 的堆栈,定位到具体是哪行代码导致永久阻塞,省去了在完整 goroutine dump 里逐个比对堆栈的时间。
适用场景
最典型的场景是长驻服务。比如后端 API 服务跑了一周后 goroutine 数量从几百涨到几万,先别急着 full dump,直接抓 goroutineleak 看看哪些 goroutine 永远不会退出。它特别适合定位以下情况:
- worker pool 里的 goroutine 因为 channel 未关闭而永久阻塞
- 持锁进入等待且永不满足条件
- 子 goroutine 没有受父生命周期控制,反复被启动后遗弃
这类问题用传统 profile 手动翻堆栈非常累,goroutineleak 几乎是直达答案。
不适用场景
它检测的是“永久阻塞”,临时阻塞、偶发退出的 goroutine 不会出现在结果里。如果想查死锁、短时间阻塞热点、goroutine 数目的瞬时抖动,传统的 /debug/pprof/goroutine 仍然更合适。另外,泄漏判定依赖 GC 可达性扫描,短生命周期进程里参考价值有限——还没跑出“永久”状态进程就退出了。最后提醒一点:goroutineleak 和传统 goroutine profile 是互补关系,前者回答“哪些 goroutine 永远不会结束”,后者回答“现在所有 goroutine 都在干什么”,配合使用效率更高。
总之,从 Go 1.27 开始,goroutine 泄漏终于有了开箱即用的检测工具,值得把这条命令加进你的排查手册。