pprof 分析内存泄漏需重点观察 alloc_space 增量而非仅 inuse_space,通过差分分析定位持续分配不释放的函数;goroutine 泄漏要关注阻塞态及增长趋势;高频泄漏源为全局变量、Ticker/Timer 和未关闭资源。
内存泄漏最直观的信号是 inuse_space 持续缓慢上涨,但很多真实泄漏(比如全局 map 不断塞入、channel 未关闭导致 goroutine 持有对象)在 inuse_space 里占比极小,单次快照根本看不出异常。这时候必须做「差分分析」:间隔 2–5 分钟分别抓两个 heap profile,用 go tool pprof -diff_base heap1.pprof heap2.pprof 查看新增分配。重点关注 alloc_space 增量大的函数——哪怕它当前 inuse_space 很低,只要持续 alloc 却不释放,就是高危点。
http://localhost:6060/debug/pprof/heap?gc=1 可强制 GC 后采样,排除临时对象干扰web 或 svg 时,右键点击函数 → list,能直接看到该函数中哪行代码分配了对象
runtime.NumGoroutine() 返回的是总数,但真正危险的是长期处于 chan receive、select 或 semacquire 的 goroutine。它们不消耗 CPU,却死死占着栈内存和引用的对象。
go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=1 查看文本堆栈,搜索 chan receive 或 IO wait,定位卡在哪一行goroutine,跑 10 分钟后再抓一次,pprof 里用 top 看新增 goroutine 的调用栈,90% 的泄漏源头就藏在这里case 后退出,但若漏掉 default 或误写成 for range ch,照样泄漏
pprof 是事后分析,而 GODEBUG 和 goleak 能在问题刚冒头时就拉响警报。尤其在测试阶段,它们比任何人工监控都可靠。
GODEBUG=gctrace=1,观察 GC 日志里 scvg(垃圾回收器收缩堆)是否频繁失败;若连续几次 scvg 都说 not enough heap,说明有对象被意外持有github.com/uber-go/goleak,在 TestMain 中调用 goleak.VerifyNone(m),它会自动捕获测试结束后残留的 goroutine,并打印初始创建位置——比翻日志快十倍GODEBUG=goprobe=1,它会显著增加调度开销;优先用 Prometheus 暴露 runtime.NumGoroutine() 指标,配 Grafana 告警阈值(如 5 分钟内增长 >200)80% 的泄漏集中在三个地方:全局 map/slice 无清理逻辑、time.Ticker 忘记 Stop()、文件/连接/通道未显式关闭。它们共同特点是“不报错、不崩溃、只悄悄吃内存”。
sync.Map 不解决泄漏,cache = make(map[string]*Item) 必须配套定时清理 goroutine 或使用 github.com/bluele/gcache 这类带 LRU 的库time.Ticker 必须配 defer ticker.Stop();time.AfterFunc 相对安全,但若传入的函数本身启动新 goroutine 且未控制生命周期,仍会泄漏os.Open、http.Client.Do、sql.DB.Query 后,立刻跟 defer xxx.Close();用 lsof -p PID 查看句柄数是否随时间线性增长,是判断资源泄漏最硬的指标真正难的不是找到泄漏点,而是确认“它为什么没被 GC”——往往是一行看似无害的赋值,让某个大对象被一个长生命周期 goroutine 意外引用。所以每次看到可疑的 inuse_objects 增长,先查它的调用栈顶端是否连着全局变量或常驻 goroutine,而不是急着改业务逻辑。