真泄漏必须满足HeapInuse和HeapAlloc随时间线性增长且重启归零;抓heap profile需启用net/http/pprof或调用pprof.WriteHeapProfile,文件后缀须为.heap或.pb.gz,采样间隔30秒以上并用-base对比inuse_space差异。

Go 程序内存“一直涨”不等于泄漏;真泄漏必须满足两个硬条件:在稳定负载下 HeapInuse 和 HeapAlloc 随时间线性增长,且每次重启后归零、再次复现。
怎么抓到真实可用的 heap profile
pprof 不会自动采样堆内存,不手动触发就什么也看不到。线上最稳的方式是提前启用 net/http/pprof,本地复现则需在关键路径前后调用 pprof.WriteHeapProfile(f)。
- 线上:启动时加
import _ "net/http/pprof",再起个 goroutine 跑http.ListenAndServe("localhost:6060", nil);然后用wget http://localhost:6060/debug/pprof/heap -O heap1.pb.gz抓快照 - 本地复现:文件名必须带
.heap或.pb.gz后缀(如before.heap),否则go tool pprof可能识别失败 - 别在程序刚启动或
MemStats.NextGC接近MemStats.HeapAlloc时采样——这时 GC 正疯狂干活,数据失真 - HTTP 采集时慎用
?gc=1:它强制 GC 后采样,会把本该被回收的对象“刷掉”,反而掩盖泄漏
为什么单看 top 常常找不到泄漏点
泄漏对象在 inuse_space 里占比往往极小,一眼扫过去全是业务主逻辑,根本看不出异常。靠“占比高”找问题,在真实泄漏场景中基本失效。
- 必须做至少两次采样(间隔 30 秒以上),用
-base参数对比:go tool pprof -http=:9999 -base heap1.pb.gz heap2.pb.gz - 浏览器打开后,
SAMPLE切换为inuse_space,再点 “View → Difference” 才能看到净增长部分 - 重点关注调用链末尾是业务代码、但中间夹着
(*ConnPool).reaper、connectionOpener、timerproc这类 runtime 或第三方库函数的路径——它们往往是泄漏入口 - 如果
top里看到*UserConfig占 150MB,但list UserConfig几乎为空,说明不是你直接&UserConfig{},而是通过json.Unmarshal解析进全局map或sync.Map.LoadOrStore持久化了指针
goroutine 泄漏为什么总和内存泄漏绑在一起
每个 goroutine 默认栈 2KB,一旦泄漏,不仅本身占内存,还极可能持有 buffer、map、channel、context 等堆对象,形成连锁泄漏。
立即学习“go语言免费学习笔记(深入)”;
- 用
runtime.NumGoroutine()打点监控,或直接访问http://localhost:6060/debug/pprof/goroutine?debug=2查看全量栈 - 错误现象:返回大量状态为
chan receive、select、time.Sleep或卡在for range ch的 goroutine,且数量随请求稳定增加 - 常见陷阱:
RedisClient.Close()漏调、*sql.DB未设SetMaxOpenConns+SetConnMaxLifetime、自定义time.Ticker未Stop() - HTTP handler 中启动 goroutine 时,务必绑定
req.Context(),并用select监听取消;否则闭包捕获的大结构体(比如含[]byte的 struct)会被钉住无法回收
pprof 看不见的泄漏场景有哪些
pprof.WriteHeapProfile 只记录当前存活的 Go 对象,但有些泄漏它完全不感知——因为 runtime 根本没把它当“Go 对象”管。
- cgo 分配的 C 堆内存:比如
C.malloc、C.CString,pprof 不统计;加GODEBUG=cgocheck=2运行看是否 panic,Linux 下可辅以valgrind --tool=memcheck -
unsafe.Pointer或reflect绕过类型系统持有的内存,pprof 无法追踪引用链 -
netpoll、timer、workergoroutine内部结构(如netpollfd表),属于 runtime 底层,不在堆 profile 覆盖范围内 - 普通
map不会自动清理键值对,sync.Map更不会;用时间戳、UUID 当 key 又从不delete,等于自己造了个内存黑洞
真正难搞的泄漏,往往藏在 goroutine 持有、map/sync.Map 持久化、闭包捕获、channel 缓冲区这些隐式引用里;而 pprof 的默认视图只给你看“谁分配了”,不告诉你“谁钉住了”。


















