go内存泄漏本质是对象被“钉住不放”,如http handler闭包、未关channel、全局map、ticker未stop;需监控heapinuse持续增长,用pprof抓满足三条件的heap快照(不加?gc=1、带.heap/.pb.gz后缀、间隔≥30秒),聚焦inuse_space线性上升对象,并通过difference+focus定位分配源。

Go 框架内存泄漏不是“分配太多”,而是“对象被钉住不放”——HTTP handler 闭包、未关闭的 channel、全局 map、time.Ticker 都可能成为钉子。真正要盯的是 runtime.MemStats.HeapInuse 持续单向增长,且 pprof 中某类对象(如 *http.Request、[]byte)的 inuse_space 随时间线性上升。
怎么抓到能定位泄漏的 heap profile
线上快照必须满足三个硬条件,缺一不可,否则看到的全是噪声:
- 绝对不要加
?gc=1:它强制 GC 后采样,会把本该长期存活的对象“刷掉”,掩盖真实泄漏点;真实泄漏对象恰恰是 GC 也收不走的那些 - 文件名必须带
.heap或.pb.gz后缀,否则go tool pprof无法识别格式 - 两次采样间隔 ≥30 秒,且程序处于稳定负载(刚启动、
MemStats.NextGC接近HeapAlloc时采样,快照里全是临时对象)
推荐命令:wget http://localhost:6060/debug/pprof/heap -O before.heap → 等 30 秒 → wget http://localhost:6060/debug/pprof/heap -O after.heap
为什么 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 它且没释放。
goroutine 泄漏才是内存缓慢上涨的真凶
90% 的“内存缓慢上涨”背后不是对象没回收,而是 goroutine 卡死,死钉着一堆大对象。比如一个闭包捕获了含 []byte 的结构体,它活着,整个结构体就永远进不了 GC。
- 查 goroutine 不要看
/debug/pprof/goroutine默认页(只给总数),必须用?debug=2:curl "http://localhost:6060/debug/pprof/goroutine?debug=2"才能看到完整堆栈和阻塞时长 - 重点盯状态为
chan receive、select、time.Sleep的 goroutine;若阻塞时长出现432000s(5 天)、1209600s(14 天)这种量级,基本 100% 是泄漏 - HTTP handler 中启 goroutine,必须绑定
req.Context(),并在select中监听ctx.Done();time.Ticker必须显式Stop(),否则 runtime 会一直持有其函数引用
框架层最常被忽略的泄漏点:http.Response.Body —— 不调 resp.Body.Close(),底层 persistConn 就不会释放,连接池会越积越多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











