goroutine泄漏典型表现为程序内存持续上涨、runtime.numgoroutine()只增不减、/debug/pprof/goroutine?debug=2中大量堆栈阻塞在chan receive/select/sync.waitgroup.wait;需对比多时间点快照确认单调增长。

goroutine 泄漏的典型表现是什么
程序内存持续上涨、runtime.NumGoroutine() 返回值只增不减、HTTP /debug/pprof/goroutines?debug=2 页面里出现大量重复堆栈(尤其是阻塞在 select{}、chan receive 或 sync.WaitGroup.Wait 上)——这些是 goroutine 泄漏最直接的信号。
pprof 本身不“检测泄漏”,它只快照当前状态;泄漏判断依赖你对比多个时间点的 /goroutines?debug=2 输出,或观察 /goroutines(默认 debug=1)中活跃 goroutine 数量是否异常偏高。
启用 pprof 并抓取 goroutine 快照的最小配置
只需在 HTTP server 中注册 pprof handler,无需额外依赖:
import _ "net/http/pprof"
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// ... your app
}
然后用 curl 抓取两种关键快照:
-
curl 'http://localhost:6060/debug/pprof/goroutines?debug=1'→ 简洁列表,适合监控数量趋势 -
curl 'http://localhost:6060/debug/pprof/goroutines?debug=2'→ 完整堆栈,必须用这个定位泄漏源头
注意:?debug=2 输出体积大,别在生产高频调用;建议先用 debug=1 确认数量异常后再抓 debug=2。
从 debug=2 输出里快速定位泄漏 goroutine
?debug=2 的输出按 goroutine 状态分组(如 “running”, “syscall”, “chan receive”),泄漏通常集中在:
- 阻塞在未关闭的 channel 接收:搜索
chan receive+ 对应 channel 变量名(如ch、done) - 空
select{}或只有default分支的select:这类 goroutine 永远不会退出 - 忘记调用
wg.Done()导致wg.Wait()卡死:搜sync.(*WaitGroup).Wait - HTTP handler 启动 goroutine 但没处理连接关闭(如
req.Context().Done()未监听)
示例片段中若看到几十个相同堆栈:
goroutine 1234 [chan receive]:
main.handleUpload(0xc000123456)
/app/upload.go:45 +0x1a2
created by main.serve
/app/server.go:88 +0x9c
就要立刻检查 handleUpload 是否在某个 channel 上永久阻塞,且该 channel 是否有对应的 sender 或 close 调用。
避免 pprof 分析时的常见误判
goroutine 数量高 ≠ 一定泄漏。以下情况属正常,但容易被误读:
- 使用
database/sql且SetMaxOpenConns设得较大 → 连接池会维持一批 idle goroutine - HTTP server 开启了
http.Server.IdleTimeout,但连接频繁建立/断开 → 短生命周期 goroutine 波动属预期行为 - 第三方库内部使用长期 goroutine(如某些日志 flusher、metrics collector)→ 查源码确认是否设计如此
真正要盯的是:同一堆栈反复出现、数量随时间单调增长、且对应业务逻辑明显不该长期存活(比如一次 API 请求本该 200ms 结束,却留下一个 goroutine 卡在 channel 上 10 分钟)。
pprof 给的是证据链起点,不是结论。拿到可疑堆栈后,必须回代码里看 channel 生命周期、context 传递、WaitGroup 配对、defer close 是否遗漏——这些细节,pprof 不会告诉你哪行少写了 close(ch)。











