真实泄漏需满足:稳定负载下HeapInuse/HeapAlloc线性增长且重启归零复现;避免刚启动或GC高频时采样,用两次HTTP快照对比diff,关注inuse_space净增长及goroutine泄漏。

怎么确认 pprof 抓到的是真实泄漏数据,不是 GC 假象
pprof 显示内存上涨 ≠ 真泄漏,关键看采样时机和指标选择。如果 heap_inuse 在稳定负载下线性增长、重启后归零复现,才值得深挖。别在程序刚启动或 MemStats.NextGC 接近 MemStats.HeapAlloc 时抓 profile——这时 GC 正高频运行,快照里全是临时对象,干扰判断。
线上必须用 HTTP 接口抓:
wget http://localhost:6060/debug/pprof/heap -O before.heap
等至少 30 秒再抓一次:wget http://localhost:6060/debug/pprof/heap -O after.heap
文件名务必带 .heap 或 .pb.gz 后缀,否则 go tool pprof 可能识别失败。
- 绝对不要加
?gc=1参数——它强制 GC 后采样,会把本该存活的对象“刷掉”,掩盖真实泄漏 - 本地复现时,用
pprof.WriteHeapProfile(f)比 HTTP 更可控,但必须在逻辑关键点前后调用(比如 handler 入口/出口、goroutine 启动前后) - 验证快照有效性:打开后切到
View → Sample,选inuse_space;如果alloc_space远高于inuse_space,说明分配多、释放快,大概率不是泄漏
为什么 top 看到 *UserConfig 占 150MB,list 却找不到分配点
因为 top 统计的是当前驻留内存总和,list 显示的是函数内所有分配(含已 GC 的),两者口径不同。你代码里可能根本没写 &UserConfig{},而是通过第三方库间接创建并持久化了指针。
典型场景包括:
-
json.Unmarshal解析进全局map[string]*UserConfig,key 永远不删 -
sync.Map.LoadOrStore("id", cfg)后忘记清理过期项 - slice 截取后赋给全局变量:
globalBuf = b[:n],底层底层数组仍被引用 - HTTP handler 闭包捕获了大结构体:
http.HandleFunc("/api", func(w, r) { data := loadBigData(); handle(r, data) })——data被闭包持有,随每次请求累积
此时 go tool pprof -http=:8080 binary before.heap after.heap 打开后,点 View → Difference,再 Focus *UserConfig,才能看到谁在 new 它且没释放。
立即学习“go语言免费学习笔记(深入)”;
调用栈里全是 runtime.selectgo 和 goroutine,怎么定位业务代码
pprof 默认只显示顶层调用栈,而泄漏往往藏在中间层。比如你看到 *model.Order 占用 80% inuse_space,但调用栈只显示 http.HandlerFunc.ServeHTTP,说明它被 handler 闭包意外捕获了。
重点盯这些 runtime 符号:
-
runtime.gopark或runtime.selectgo:优先查 channel 缓冲区满、for range ch前没人close(ch)、context.WithCancel忘记 cancel -
timerproc或time.AfterFunc:time.Ticker忘记Stop(),闭包里所有变量全被钉住 -
(*ConnPool).reaper或connectionOpener:数据库/Redis 客户端未设SetMaxOpenConns或SetConnMaxLifetime
用 go tool pprof -symbolize=none 绕过符号解析失败问题,避免调用栈显示为 ???;Web UI 中点 Call graph,从 runtime 节点往上翻,直到出现你的包路径(如 myapp/handler.go)。
goroutine 泄漏为什么总让 heap 分析失效
每个 goroutine 栈初始仅 2KB,但它只要活着,就能 hold 住任意大的堆对象。比如一个后台 goroutine 持有 chan []byte,或闭包捕获了含 map[string][]byte 的 struct,pprof heap 就看不到泄漏源头——内存是被活跃 goroutine “钉住”了,GC 不敢回收。
必须同步检查 goroutine profile:
- 访问
http://localhost:6060/debug/pprof/goroutine?debug=2,看是否有几百个状态为chan receive或select的 goroutine,且数量随请求稳定增加 - HTTP handler 中启 goroutine 时,务必绑定
req.Context(),并用select { case 提前退出 - 所有
go func() { }()都要检查退出路径:有没有for循环卡死?有没有time.Sleep没配超时?有没有 channel 写入没人读?
最常被忽略的是:goroutine 本身不占多少内存,但它持有的 channel、buffer、map、context 等才是真正的内存大户——定位不到堆泄漏,先查 goroutine 是否失控。


















