确认内存泄漏需对比两次pprof inuse_space采样,观察top -cum中持续增长且不回落的类型;若runtime.mallocgc占比高则是高频分配非泄漏;heapinuse与alloc差值长期>200mb才提示真实泄漏。

pprof heap 看到 inuse_space 持续上涨,怎么快速确认是不是真泄漏
不是所有内存上涨都叫泄漏——GC 还没触发、对象还在活跃期、或者只是缓存预热,都会让 inuse_space 看起来在涨。关键得看「涨得有没有道理」。
- 先对比两次采样:
go tool pprof -inuse_space http://localhost:6060/debug/pprof/heap,等 3–5 分钟再采一次,top -cum看哪些类型在持续增加且不回落 - 重点盯
runtime.mallocgc占比:如果它在火焰图里占比高,说明分配太猛,不是泄漏,是高频堆分配 - 用
go tool pprof -alloc_objects查谁在疯狂 new 对象;用-inuse_objects查谁长期赖着不走 - 检查
runtime.ReadMemStats中的HeapInuse和Alloc差值:如果差值长期 > 200MB,大概率有未释放引用(比如全局map里 key 没删干净)
strings.Builder 为什么比 fmt.Sprintf 或 + 拼接快得多
因为 fmt.Sprintf 和字符串 + 在循环里每次都会新分配堆内存,而 strings.Builder 复用底层数组,避免重复 malloc。
-
fmt.Sprintf("%s:%d", s, i):每次调用都新建[]byte→ 转string→ 堆分配 -
s1 + s2 + s3:Go 会生成临时数组拼接,中间结果无法复用 -
strings.Builder内部用[]byte缓冲,WriteString不分配新 string,String()只做一次底层数据转换 - 实操建议:循环内拼接必用
strings.Builder;单次拼接无所谓,但养成习惯能防误入 hot path
var b strings.Builder
b.Grow(1024) // 预分配,避免首次 Write 扩容
for _, v := range items {
b.WriteString(v.Name)
b.WriteByte(':')
b.WriteString(strconv.Itoa(v.ID))
}
result := b.String() // 仅此处一次堆分配
sync.Pool 复用 *bytes.Buffer 却发现内存更高了,问题出在哪
sync.Pool 不是“用了就省内存”的开关,它只对生命周期短、创建贵、大小稳的对象有效;用错反而加重 GC 压力。
- 危险信号:
Get()后没调Reset()→ 上次残留数据导致后续Write触发扩容 - 更隐蔽的问题:把含指针字段的结构体塞进 Pool(比如带
map或chan的 struct),GC 扫不到 Pool 里的对象,造成隐性泄漏 - 命中率低时反拖累:如果
Get()经常返回 nil(即池空),等于白加一层 mutex 开销;可用go tool pprof -alloc_objects看sync.Pool.Get是否真减少了分配次数 - 正确姿势:只放
*bytes.Buffer、*json.Decoder这类明确无外部引用、构造开销大的对象;每次Put前必须buf.Reset()
切片反复 make([]T, 0) 导致 GC 频繁,怎么安全复用
循环里写 make([]int, 0) 看似轻量,实则每次都在堆上分配新底层数组,旧数组只能等 GC 回收——尤其在 HTTP handler 这种每秒千次的场景下,压力直接拉满。
- 错误写法:
for _, v := range req.Items { data := make([]byte, 0, 128); ... }→ 每次都新分配 - 推荐做法:在 handler 外层或中间件中预分配,用
slice = slice[:0]清空长度,保留容量 - 注意别踩坑:
slice[:0]安全,但slice[0:0]和slice[:0:cap(slice)]效果一样;别用nil切片直接append,它会在第一次 append 时分配 - 极端情况(如不确定最大长度):配合
sync.Pool管理预分配切片,但务必Put前截断为[:0],否则下次Get可能拿到脏数据
真正卡住内存的,往往不是某行代码写错了,而是 goroutine 持有 channel、context 没 cancel、timer 忘 stop,或者 map 里 key 是 time.Time 这种带纳秒精度的值——看着只占一点内存,百万条就是几百 MB 且永不释放。定位时别只盯着堆,得同步看 /debug/pprof/goroutine?debug=2 和 runtime.NumGoroutine() 的趋势。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











