sync.Pool 仅在对象满足“高频分配+短生命周期+可安全重置”时才降低 GC 压力;误用会导致 allocs/op 上升、heap_alloc 滞留或 use-after-free panic。

sync.Pool 不是“用了就减 GC”的开关,它只在对象满足「高频分配 + 短生命周期 + 可安全重置」时才真正降低 GC 压力;用错反而让 allocs/op 上升、heap_alloc 滞留、甚至引发 use-after-free panic。
Get 到的对象必须手动 Reset,不能依赖 New 初始化
从 sync.Pool.Get() 拿到的对象不是新实例,它可能残留上一次的字段值、slice 长度或 map 内容。比如 *bytes.Buffer 的 buf.Bytes() 可能返回旧响应体,len(buf.Bytes()) 不为 0。
-
bytes.Buffer必须调buf.Reset()(清长度,不 realloc) - 自定义结构体含
[]byte字段:需s.Data = s.Data[:0] - 含
map[string]int字段:需mapclear(s.Cache)或置nil后make - 含指针字段(如
*http.Request):必须显式设为nil,否则引用滞留会阻止 GC 回收 - 别在
New里做初始化——它只在池空时触发,且不保证每次Get都调用
New 函数必须返回全新对象,且不能有副作用
New 是兜底工厂,不是初始化钩子。它的唯一职责是:池空时快速造一个干净对象。一旦返回共享实例(如全局变量地址),多个 goroutine 并发读写就会污染数据。
- 错误写法:
New: func() interface{} { return &globalBuf }—— 全局变量被所有 P 共享,必然竞态 - 正确写法:
New: func() interface{} { return &MyStruct{Data: make([]byte, 0, 1024)} }—— 每次都 new + 预分配 - 禁止在
New里做耗时操作:如os.Open、http.Get、加锁、访问未同步的全局 map - 若对象需固定容量(如 4KB buffer),必须在
New里make完,否则复用失去意义
Put 前必须确保对象已彻底脱离所有 goroutine 作用域
sync.Pool.Put() 不检查引用,也不做任何保护。一旦放入池,对象可能下一毫秒就被别的 goroutine Get 走。此时若原 goroutine 还在读写它,就是典型的 use-after-free。
- 避免无条件
defer pool.Put(x):尤其当函数启动了 goroutine、写入 channel、或闭包捕获了x时 - 安全时机是逻辑彻底结束之后:没传参给其他 goroutine、没作为返回值传出、没注册进 callback
- HTTP handler 中,
Put应放在w.Write()之后、函数 return 之前,而非开头 defer - 对
io.Copy(dst, src)类场景,Put必须等 copy 完成后再手动调用,不能靠 defer
验证是否真正在降低 GC 压力,而不是囤积内存
写了 sync.Pool 不等于性能变好。很多情况下,对象太小(如几个 int 字段的 struct)、生命周期过长、或 Put 太晚,都会导致池变成内存黑洞。
- 用
go test -bench . -benchmem -cpuprofile=cpu.pprof对比:关注allocs/op是否下降,而非仅看 CPU 时间 - 开启
GODEBUG=gctrace=1观察scvg和heap_alloc波动:若 GC 频次没降甚至上升,说明池没起效 - 调用
runtime.ReadMemStats查MStats.Mallocs - Frees差值:长期不收敛 = 池在囤积对象 - 用
go tool pprof http://localhost:6060/debug/pprof/heap?debug=1看对象分配来源,确认热点是否真的从new移到了Pool.Get - 单个对象建议控制在几 KB 内;超过 32KB 可能绕过 mcache,Pool 效果锐减
真正关键的不是“加不加 sync.Pool”,而是确认你的对象是否同时满足三个硬条件:创建开销大、生命周期短于单个 goroutine、状态可完全重置。其余时候,让 Go 自己分配,往往更省心。


















