真实泄漏需满足heapinuse在稳定负载下线性增长且重启归零复现;启用pprof须导入_net/http/pprof_并运行http.listenandserve,采集时访问/debug/pprof/heap不带?gc=1参数,保存为.heap或.pb.gz后缀文件,间隔30秒以上多次采样后用go tool pprof -base对比inuse_space趋势。

真泄漏不是“内存涨了”,而是 runtime.MemStats.HeapInuse 在稳定负载下持续线性增长,且每次重启归零、再次复现;单次快照毫无意义,必须间隔 30 秒以上连续采样 3–5 次,用 pprof 对比 inuse_space 趋势。
怎么启用并抓到有效的 heap profile
线上服务不启 net/http/pprof 就等于没开诊断开关。必须在 main 包里加这行:import _ "net/http/pprof"(下划线不能漏),再起一个 goroutine:go http.ListenAndServe("localhost:6060", nil)。
采集时别用 ?gc=1:它强制 GC 后采样,会把本该存活的对象“刷掉”,掩盖真实泄漏。正确做法是直接访问 http://localhost:6060/debug/pprof/heap(不带参数),wget 保存为 heap1.pb.gz 或 heap1.heap(后缀必须是 .heap 或 .pb.gz,否则 go tool pprof 可能识别失败)。
- 容器部署时注意端口映射:宿主机要能访问到容器内
6060端口,Docker 运行需加-p 6060:6060 - 别在程序刚启动或
MemStats.NextGC接近MemStats.HeapAlloc时采样——GC 正忙,数据失真 - 本地复现可手动调
pprof.WriteHeapProfile(f),但务必确保f是带.heap后缀的文件句柄
为什么 top 看不到你的函数名
top 里全是 runtime.mallocgc、bytes.makeSlice,业务函数名压根不出现?这不是 pprof 坏了,而是 Go 的逃逸分析把分配动作“藏”到了底层。
真正分配点往往在标准库或 runtime 内部:比如 append 扩容、json.Unmarshal 解析、fmt.Sprintf 拼接字符串。这些操作本身不显示你的函数名,但调用栈里有线索。
- 用
list 函数名(大小写敏感)看具体哪一行触发大量分配;若报no source found,说明编译时用了-ldflags="-s -w"去符号,得去掉 - 用
web生成火焰图,放大后能看到完整链路,例如main.handleRequest → json.Unmarshal → runtime.growslice - 如果
top -cum里flat很小但cum很大,说明你函数只是中转站,真正分配发生在下游调用里
对比两次快照才能定位泄漏点
单次 heap.pprof 只是一张快照,看不出增长。必须至少两次采样(建议 3–5 次,间隔 ≥30 秒),用 -base 参数做差分:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
go tool pprof -http=:9999 -base heap1.heap heap2.heap
浏览器打开后,右上角 SAMPLE 切到 inuse_space,再点 View → Difference,才能看到净增长部分。
- 重点关注调用链末尾是业务代码、中间夹着
(*ConnPool).reaper、connectionOpener、timerproc的路径——它们往往是泄漏入口 - 如果
alloc_space远大于inuse_space,说明分配多、释放少,更可能是泄漏;若两者接近,大概率是长生命周期对象(如全局map缓存) - 用
Focus *YourStruct+Call graph,能快速定位谁 new 了它、又谁一直 hold 着没放
goroutine 泄漏常被当成内存泄漏
每个 goroutine 默认栈仅 2KB,但它只要活着,就能钉住任意大的堆对象——比如闭包捕获了含 []byte 的结构体,或后台 goroutine 持有数据库连接池引用。这时 /debug/pprof/heap 看不到源头,但 runtime.NumGoroutine() 会持续上升。
查 goroutine 必须用 ?debug=2:curl "http://localhost:6060/debug/pprof/goroutine?debug=2",重点盯状态为 chan receive、select、time.Sleep 的 goroutine。
- 常见陷阱:
RedisClient.Close()漏调、sql.DB未设SetMaxOpenConns和SetConnMaxLifetime、自定义time.Ticker忘记Stop() - HTTP handler 中启 goroutine,必须绑定
req.Context(),并在select中监听ctx.Done() - 普通
map或sync.Map用时间戳、UUID 当 key 却从不delete,等于自己造内存黑洞
最易被忽略的是 cgo 分配的 C 堆内存(如 C.malloc)、unsafe.Pointer 绕过类型系统持有的内存,以及 netpoll、timer 等 runtime 底层结构——pprof 根本看不见它们。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










