真泄漏需满足heapinuse与heapalloc在稳定负载下线性增长、重启归零复现且gc后不回落;抓heap profile须禁用?gc=1、文件名带.heap或.pb.gz后缀、间隔≥30秒采样,再用pprof -http差分聚焦inuse_space定位泄漏源。

Go 框架本身不直接导致内存泄漏,真正出问题的是你用框架时写的代码——比如 HTTP handler 闭包捕获大结构体、中间件里忘了清理 context.Value、全局 sync.Map 不清过期项,或者用 json.Unmarshal 直接塞进 map[string]*T 后永不删除。
怎么确认是真泄漏,不是 GC 滞后或缓存膨胀
别光看 top 命令或监控图上内存“一直在涨”。真泄漏必须同时满足:
• 稳定负载下 runtime.ReadMemStats 的 HeapInuse 和 HeapAlloc 随时间线性增长
• 每次重启后归零,且相同流量下再次复现
• GC 后 HeapInuse 不回落,甚至越 GC 越高
如果只是压测刚启动时内存飙升、几分钟后就平稳,大概率是 GC 还没热起来,或框架内部缓存(如 http.Server 的 connection pool)在预热,不是泄漏。
抓 heap profile 必须避开的三个坑
- 绝对不要加
?gc=1:它强制 GC 后采样,会把本该长期存活的对象“刷掉”,profile 里只剩临时对象,完全掩盖泄漏 - 文件名必须带
.heap或.pb.gz后缀:比如before.heap,否则go tool pprof可能识别失败,报错 “unrecognized profile format” - 别在程序刚启动或
MemStats.NextGC接近HeapAlloc时采样:这时 GC 正疯狂 sweep,快照里全是待回收垃圾,数据失真
线上最稳方式:
• 启动时导入 _ "net/http/pprof",起个 goroutine 跑 http.ListenAndServe("localhost:6060", nil)
• 等服务稳定运行 5 分钟以上,再执行:wget http://localhost:6060/debug/pprof/heap -O before.heap
• 等 30 秒(确保有足够请求进来),再抓一次:wget http://localhost:6060/debug/pprof/heap -O after.heap
用 pprof 差分定位泄漏点的关键操作
执行:go tool pprof -http=:8080 -base before.heap after.heap
浏览器打开后注意三步:
- 右上角 SAMPLE 切到
inuse_space(不是alloc_objects或alloc_space) - 点 View → Difference(不是 Top 或 Flame Graph)——只有差分才能看到净增长部分
- 在搜索框 Focus 输入你的可疑类型,比如
*UserConfig或model.Order,再点 Call graph 查谁在 new 它且没释放
重点盯调用链里夹着这些关键词的路径:
• (*ConnPool).reaper、connectionOpener → 检查数据库连接池未设 SetMaxOpen 或 Close 漏调
• timerproc、time.AfterFunc → 检查 time.Ticker 或 time.After 创建后没 Stop
• http.HandlerFunc.ServeHTTP 下直接连着业务 struct → 说明 handler 闭包捕获了大对象(比如 loadBigData() 返回的 []byte)
为什么 goroutine 泄漏总和内存泄漏一起出现
每个 goroutine 默认栈才 2KB,但它只要活着,就能 hold 住任意大的堆对象。常见组合:
- goroutine 卡在
chan receive或select,同时闭包里捕获了map[string][]byte - 后台 goroutine 持有
*sql.DB或*redis.Client,而 client 内部又持有大量 buffer 和连接 - HTTP 中间件用
context.WithValue塞了大结构体,但 handler 返回后 context 没被回收(因为被子 goroutine 持有)
先快速验证:
• 打点 runtime.NumGoroutine(),看压测后是否不回落
• 访问 http://localhost:6060/debug/pprof/goroutine?debug=2,搜 chan receive、select、io.ReadFull,看数量是否随请求稳定增加
• Go 1.24+ 可试 GODEBUG=goleak=1 + runtime.GC() 后访问 /debug/pprof/goroutineleak
pprof 看不到 cgo 分配、unsafe.Pointer 持有的内存,也看不到 netpoll 底层结构;如果差分后增长全在 runtime 或 C 侧,得换 valgrind 或 GODEBUG=cgocheck=2 排查。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











