sync.Pool 适合高频创建、短生命周期、类型固定的对象复用场景,如 HTTP 中的 bytes.Buffer、请求上下文、JSON 临时 map;不适用于长生命周期、需重置字段多或大小波动大的对象。

sync.Pool 适合什么场景
sync.Pool 不是万能的缓存,它只在「高频创建 + 短生命周期 + 类型固定」的对象上才真正减少 GC 压力。比如 HTTP 中的 bytes.Buffer、自定义的请求上下文结构体、JSON 解析用的临时 map[string]interface{}。如果你的对象生命周期长(比如被塞进全局 map)、或者每次取出来都要重置大量字段、又或者大小波动极大(从 16B 到 2KB 都有),sync.Pool 反而会因清理不及时或内存碎片拖慢性能。
- 对象必须可被安全复用:不能含未清零的指针、闭包、或外部引用
- 最好配合
New字段做兜底构造,避免 Get 返回 nil - 每个 P(逻辑处理器)独占一个子池,跨 P 的 Put/Get 不共享,所以不适合“全局唯一实例”类需求
正确初始化和使用 New 字段
sync.Pool 的 New 函数只在 Get 发现当前 P 的本地池为空时才调用,且不加锁——这是它快的关键。但很多人漏写 New,导致第一次 Get() 返回 nil,接着 panic。
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}-
New必须返回新分配的对象,不能返回局部变量地址或复用已有实例 - 如果对象需要预分配容量(如
bytes.Buffer常用make([]byte, 0, 1024)),就在New里做,别等 Get 后再Grow - 不要在
New里做耗时操作(如打开文件、网络请求),它可能在任意 Goroutine 中被触发
Put 和 Get 的时机必须严格匹配
最常见错误是:Get 出来用了,但忘了 Put 回去;或者 Put 了两次;或者在 Goroutine 退出前没 Put(尤其 defer 里写错顺序)。结果就是对象永远卡在某个 P 的本地池里,既没被复用,也没被 GC 回收(因为 Pool 不强制回收),最终内存缓慢上涨。
- 每次
Get()后,必须确保有且仅有一次对应的Put() - 推荐模式:Get → 使用 → defer Put(注意 defer 在函数退出时执行,确保不遗漏)
- 禁止 Put 已被修改为不可复用的对象:比如把一个
bytes.BufferPut 前调用了Buffer.Reset()是安全的;但如果它曾WriteString过超大字符串导致底层数组膨胀,Put 回去就浪费内存
注意 Pool 的清理时机和 GC 影响
sync.Pool 在每次 GC 开始前会清空所有本地池(包括已 Put 但尚未被 Get 的对象)。这意味着:
它不是长期缓存,不能依赖它“记住”上次计算结果
如果你的对象构造成本高(比如解析 YAML 模板),但使用频率低,Pool 可能刚缓存完就被 GC 清掉,白忙一场
多次短间隔 GC(如内存紧张时)会让 Pool 效果归零,此时应优先排查内存泄漏或调整 GOGC
可通过
runtime.ReadMemStats对比开启/关闭 Pool 时的Mallocs和HeapAlloc变化,验证是否真减少了分配生产环境别盲目加 Pool:先用 pprof 查到具体哪行
new或make占比高,再针对性优化
Pool 的真正价值不在“省多少内存”,而在于把堆分配从高频、随机、小块,变成低频、集中、可控——但前提是,你清楚知道每个 Get 背后对应哪个 Put,以及那个对象在被 Put 前,是否真的干净。


















