sync.pool 的 get/put 不是“存啥取啥”,因其不保证对象持久性:put 的对象可能被 gc 清除、被其他 goroutine 偷走,get 优先从 p 的 private 字段获取,再查本地 shared 队列、偷其他 p 的 shared 尾部、fallback 到 victim 缓存,全失败才调用 new;返回对象不一定是 put 过的,甚至非同批初始化,且 gc 前全部清空。

sync.Pool 的 Get/Put 为什么不是“存啥取啥”?
因为 sync.Pool 不保证对象持久性——你 Put 进去的对象,可能在下一次 Get 前就被 GC 清掉了,或者被别的 goroutine “偷走”了。这不是 bug,是设计使然。
它的核心逻辑是:优先从当前 P(processor)的 private 字段拿;没拿到就查本地 shared 队列(需加锁);再没就遍历其他 P 的 shared 尾部“偷”(也加锁);最后 fallback 到 victim 缓存(上一轮 GC 前保留的池快照),全失败才调用 New。
-
Get返回的不一定是你Put过的那个实例,甚至不一定是同一批初始化的对象 -
Put成功 ≠ 对象一定还在池里;GC 前所有池内容都会被清空(包括victim) - 不要在
Put前依赖对象字段还保持上次使用时的状态——必须手动重置
什么时候该用 sync.Pool?别硬套
它只适合高频创建、生命周期短、初始化开销大、且状态可重置的临时对象。不是所有“想复用”的场景都合适。
- ✅ 合适:json 解码器(
json.Decoder)、http 临时缓冲区(如bytes.Buffer)、字符串拼接器、小结构体切片 - ❌ 不合适:数据库连接(需要健康检查和生命周期管理)、带外部资源句柄的对象(文件描述符、net.Conn)、有内部 goroutine 或 channel 的对象(可能泄漏)
- ⚠️ 特别注意:作为短生存期对象内部维护的 free list(比如某个 Struct 自己管几个子对象),用
sync.Pool反而增加锁开销,不如自己实现无锁链表
如何避免 sync.Pool 引发内存泄漏或 panic?
最常见错误是闭包捕获外部变量、未清空指针字段、或类型断言失败。这些不会立刻报错,但会在高并发下暴露。
立即学习“go语言免费学习笔记(深入)”;
- Put 前务必清空对象中所有可能指向长生命周期数据的字段(如
slice的底层数组、map、chan、指针成员) - 不要在
New函数里引用外部变量(比如捕获循环变量i),否则所有池对象共享同一份闭包环境 - Get 后必须做类型断言,且要处理
nil情况:v, ok := pool.Get().(*MyStruct),不能直接强转 - 池子本身不能复制——声明后就该作为包级变量或结构体字段长期持有,别传参或赋值给新变量
性能优化的关键:控制 local 和 shared 的平衡
Go 运行时为每个 P 分配一个 poolLocal,含 private(无锁,单值)和 shared(加锁,切片)。真正影响吞吐的是这两层怎么用。
- 高频小对象(如
struct{a,b int})尽量走private:Put 时若l.private == nil才放进去;Get 时优先取它 - 避免频繁触发
shared的锁竞争——比如多个 goroutine 在同一 P 上密集 Put/Get,会卡在shared.pushHead或popHead - 没有“池大小限制”,但
shared是切片,不断增长会引发内存碎片;建议在New中预分配合理 cap(如make([]byte, 0, 1024))
真正难的不是写对 Get/Put,而是判断这个对象是否值得进池、以及怎么让它干净地进出——状态重置漏一行,就可能让后续 goroutine 读到脏数据。