sync.pool 的 private 池只存一个对象,因其在 poollocal 结构中为单个 interface{} 类型,设计目标是“快速命中”刚放回的最新对象,实现零锁复用;反复 put 会覆盖前值,仅最后一次生效。

sync.Pool 的 private 池为什么只存一个对象
因为 private 字段在 poolLocal 结构里是单个 interface{} 类型,不是切片或链表。它设计初衷就是“快速命中”——当前 P 正在运行的 goroutine 刚刚放回、且尚未被其他 P 抢走的对象,直接复用,零锁、零原子操作。
常见错误现象:有人误以为 private 可以缓存多个对象,于是反复 Put 多次,结果只有最后一次生效,前面的都被覆盖丢弃了。
-
Put时若l.private == nil,就直接赋值;否则跳过,把对象塞进shared -
Get时取完立刻置l.private = nil,防止重复使用同一对象却不重置状态 - 这个“一进一出”机制决定了它不适合缓存批量对象,只适合高频单例复用(如临时
[]byte缓冲区)
shared 池为何用 poolChain 而非 slice
shared 池底层是 poolChain,一个无锁的、支持并发 pushHead/popHead 和 popTail 的链表结构。这直接服务于“窃取(steal)”场景:当本地 private 和 shared 都空了,Get 会遍历其他 P 的 shared,从尾部 popTail —— 这样能尽量拿走最“老”的对象,减少干扰本地活跃对象的局部性。
如果用 []interface{},每次 append 或 copy 都要加锁,或者引入复杂原子操作,性能反而更差。
-
poolChain的每个节点是固定大小数组(默认 8 个元素),内存局部性好 -
popTail是“偷”的关键:它不修改头部指针,只读尾部,避免与其他 P 的pushHead冲突 - 注意:
shared操作需加锁(l.Lock()),但锁粒度仅限单个 P 的 shared,不是全局锁
“窃取”失败后为什么还要查 victim 缓存
Go 1.13 引入 victim 是为了解决 GC 周期间对象“过早丢失”问题。每次 GC 开始前,runtime 会把所有 P 的 local 池整体移到 victim,并清空 local;GC 完成后,victim 不会被立即销毁,而是留作下一轮 Get 的兜底 —— 这些对象已经熬过一次 GC,大概率还能再活一轮。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
容易踩的坑:有人以为 victim 是长期缓存,其实它只存在一个 GC 周期。两次 GC 之间没被取走的对象,会在下一次 GC 清理时直接丢弃,不会累积。
-
getSlow中先遍历所有 remoteshared,最后才查victim,顺序不能颠倒 -
victim查不到才调New,所以New函数不应有副作用(比如发 HTTP 请求),否则可能被意外多次触发 - 如果你的应用 GC 频繁(比如
GOGC=10),victim实际命中率很低,这时应优先优化对象生命周期,而非依赖 victim
为什么 Put 有 1/4 概率直接丢弃对象
这是 sync.Pool 内置的随机丢弃策略,在 Put 开头就有:if fastrand()%4 == 0 { return }。目的很实际:防止单个 P 的 shared 池无限膨胀,尤其当对象分配速率远高于消费速率时。
这不是 bug,是 feature。它让 Pool 更像一个“软缓存”而非“硬存储”,与 GC 协同控制内存水位。
- 该丢弃只发生在
Put阶段,且仅影响shared池(private不参与) - 丢弃概率固定为 25%,不可配置;想绕过?别放那么多,或确保对象一定被
private接收 - 如果你观察到
Get命中率持续低于 30%,先检查是不是对象没重置状态导致被下游拒绝复用,而不是怪丢弃逻辑
真正难处理的从来不是“怎么用”,而是“怎么让对象干净地进出”。Pool 不管你对象内部有没有残留字段、是否关闭了文件描述符、是否还持有 channel 引用 —— 它只管内存块。一旦忘了在 Put 前清空状态,bug 就会以竞态或 panic 形式在某个深夜出现。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










