sync.Pool.Get 和 Put 本身线程安全,但复用对象需手动重置状态;未清理的指针、切片或锁字段易引发 panic 或竞态,且 GC 会清空所有对象,故 Pool 仅是临时对象回收站而非缓存。

sync.Pool.Get 和 Put 本身是线程安全的
sync.Pool 的 Get 和 Put 方法内部已做并发控制,多个 goroutine 可同时调用,不会 panic 或数据错乱。它的线程安全性来自底层按 P(逻辑处理器)分片的设计:每个 P 拥有独立的 private 字段(无锁)和 shared 切片(加锁访问),绝大多数操作落在本地 P 上,避免全局锁竞争。
但要注意:线程安全 ≠ 对象安全。Pool 不保证你 Get 到的对象处于“干净”状态——它可能来自任意 goroutine 的上次 Put,字段值完全不可控。所以真正决定是否线程安全的,是你对对象本身的使用方式。
为什么 Get 到的对象可能引发竞态或 panic
常见错误现象包括:nil pointer dereference、字段值异常、slice bounds out of range,根源几乎都出在复用时没重置状态。例如:
- 结构体里存了
*sync.RWMutex,Put 前没重置为nil,下次 Get 后直接mu.RLock()—— 实际调用的是上一次残留的已销毁 mutex - 切片字段如
data []byte,Put 前只做了data = data[:0],但底层数组仍被其他字段引用,导致后续append覆盖旧数据 - 指针字段指向堆上大对象,未置空,造成该大对象无法被 GC,内存持续上涨
这些都不是 sync.Pool 本身的 bug,而是对象复用契约被破坏的结果。
Put 前必须手动清理,不能依赖 New 或构造函数
sync.Pool.New 只在 Get 返回 nil 时触发,且不保证只调用一次(GC 后会再触发)。它不是初始化入口,更不是“兜底 reset”。典型误用:
var pool = sync.Pool{
New: func() interface{} {
return &MyObj{
buf: make([]byte, 0, 1024),
mu: &sync.RWMutex{}, // ❌ 错!复用后 mu 已失效
}
},
}
正确做法是把 reset 逻辑显式写在 Get 之后:
b := pool.Get().(*MyObj) b.Reset() // 必须自己定义 Reset(),清空所有可变字段、置空指针、重置切片长度和容量 defer pool.Put(b)
关键点:
-
Reset()要覆盖所有可变状态,不能只清零部分字段 - 不要在
Put里做耗时操作(如关闭文件、释放网络连接),Pool 会阻塞调用方 - 别把对象传给其他 goroutine 后再
Put——Put必须和Get在同一个 goroutine 内完成
GC 会让复用率远低于预期,别高估 Pool 的“常驻”能力
每次 GC 触发前,sync.Pool 中所有对象都会被强制丢弃,不管是否正在被使用(只要没有外部强引用)。这意味着:
- 如果两个
Get调用之间隔了一次 GC,第二次大概率拿到New创建的新对象,而非复用 - 长期运行的 goroutine 若反复
Get/Put,但中间经历多次 GC,实际复用率可能趋近于 0 - 用
go build -gcflags="-m"检查对象是否逃逸 —— 逃逸到堆上的对象,其字段若引用大内存块,会导致整个块被钉住,抵消 Pool 效果
最易被忽略的一点:Pool 不是缓存,也不是连接池;它只是“临时对象的快速回收站”,设计目标从来就不是保活,而是降低分配频次。指望它长期持有热对象,等于用错了工具。


















