sync.Pool仅在高频、短生命周期、无状态对象场景下有效降低GC压力;New必须返回全新对象,禁止复用全局变量,Get后须重置状态,Put前需确保对象已脱离作用域,否则引发竞态或use-after-free。

sync.Pool 不是“加了就快”,它只在高频、短命、无状态对象场景下才真正降低 GC 压力;用错反而增加竞态风险和内存扫描负担。
sync.Pool.New 必须返回全新对象,不能复用全局变量
这是最常踩的坑:New 函数若返回同一个指针(比如 &globalBuf 或 sharedStruct),多个 goroutine 并发 Get() 会拿到同一块内存,导致数据污染或 panic。
- 错误写法:
New: func() interface{} { return &myGlobalBuffer }—— 全局变量被所有 goroutine 共享 - 正确写法:
New: func() interface{} { return &bytes.Buffer{} }或New: func() interface{} { return make([]byte, 0, 1024) }—— 每次调用都分配新内存 - 对 slice/map 类型,New 中必须初始化容量(如
make(map[string]interface{}, 0)),否则 Put 后残留旧键值,下次 Get 直接读到脏数据
Get 后必须重置状态,不能依赖初始值
Get() 返回的对象可能来自上一次使用,内部字段、slice 长度、map 内容全都不清空。不重置就直接用,等于拿别人用过的碗吃饭。
- 对
*bytes.Buffer:必须调用buf.Reset()(比buf.Truncate(0)更安全,会同时清空 cap 外部引用) - 对自定义结构体:显式重置字段,例如
buf.pos = 0; buf.data = buf.data[:0] - 对 map:不能只清空 key,要
for k := range m { delete(m, k) }或更高效地m = make(map[string]interface{})(然后 Put 新 map)
Put 前必须确保对象已完全脱离作用域
Put() 是“移交所有权”,不是“通知回收”。Pool 不做引用计数,一旦 Put 进去,对象可能下一毫秒就被别的 goroutine 取走。此时若原 goroutine 还在读写,就是典型的 use-after-free。
- 避免在
defer里无条件Put():如果函数启动了异步 goroutine 并传入该对象指针,defer 执行时对象可能还在被使用 - 安全时机:业务逻辑彻底结束、未发送到 channel、未闭包捕获、未启动后续 goroutine
- 特别注意 HTTP handler 中的
*http.Request:不能直接 Put 原始r,而应 Put 自己 New 的副本,并确保r.Header.Clone()等操作已完成
对象大小和生命周期决定 Pool 是否真有效
Pool 效果高度依赖实际运行特征,不是所有“临时对象”都适合池化。
- 单个对象建议 ≤ 8KB;超过 32KB 会绕过 mcache,进入大对象分配路径,Pool 基本失效
- 适用场景:QPS ≥ 1k 的 HTTP 请求处理、JSON 编解码缓冲、短生命周期 DTO 构造
- 不适用场景:低频调用(命中率趋近于 0)、生命周期长(如连接池)、本身已复用(
http.Request在 net/http 内部已有复用机制) - 验证是否生效:开
GODEBUG=gctrace=1对比 GC 次数和heap_alloc增速;或用runtime.ReadMemStats查MStats.NumGC和PauseNs
Pool 的本质是“尽力复用”,不是“一定缓存”——它不保证 FIFO/LRU,不防 GC 清理,也不跨 P 稳定保留。上线前必须压测,而不是靠 New 函数写对了就以为万事大吉。


















