sync.pool 是仅在 gc 周期间临时存放可复用对象的本地缓存,不保证复用;get() 可能返回 nil,需检查并兜底构造;put() 前必须显式 reset();对象按 p 分片,跨 p 不共享,gc 时批量清理。

sync.Pool 不是“缓存”,也不是“对象池”意义上的通用复用工具;它只在 GC 周期间临时存放可复用对象,且不保证一定被复用——这是理解它行为的前提。
为什么 Get() 有时返回 nil?
因为 sync.Pool 不保证存放的对象一定存活到下次 Get() 调用:GC 触发时,所有未被引用的池中对象会被批量清理;如果没提前 Put() 过对象,或上次 Put() 的对象已被回收,Get() 就只能返回 nil。
常见错误现象:panic: runtime error: invalid memory address,源于直接解引用 Get() 返回的 nil 指针。
- 必须检查
Get()返回值是否为nil,并提供兜底构造逻辑 - 不要依赖
Put()后对象“一定还在池里”,尤其跨 goroutine 或长生命周期场景 -
New字段只在Get()返回nil时触发一次,不是每次调用都执行
示例:
立即学习“go语言免费学习笔记(深入)”;
var bufPool = &sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
buf := bufPool.Get().(*bytes.Buffer)
if buf == nil {
buf = new(bytes.Buffer) // 实际不会执行,因 New 已定义
}
buf.Reset() // 必须重置状态,不能假设内容为空
Put() 前为什么要手动 Reset()?
sync.Pool 不知道你的类型结构,也不会自动清空字段;若直接 Put() 一个已用过的对象,下次 Get() 可能拿到残留数据、未关闭的资源或 panic 状态。
使用场景:频繁分配 []byte、strings.Builder、自定义结构体等。
- 所有可复用对象在
Put()前必须显式归零/重置,比如buf.Reset()、slice = slice[:0]、struct.field = "" - 避免在
New函数里做重置——那是构造新对象用的,不是复用路径 - 对含指针或系统资源(如文件描述符)的类型,
Reset()还需释放非内存资源
sync.Pool 在 P 级别本地缓存,跨 P 不共享
每个 P(Goroutine 调度上下文)维护独立的私有池子,Get()/Put() 默认操作当前 P 的本地池;只有本地池为空时,才尝试从其他 P “偷”对象。这带来两个关键影响:
- 高并发下,对象大概率留在原 P,减少锁竞争,但也会导致“池子冷热不均”——某些 P 池满,另一些却总
Get()到nil - goroutine 被抢占迁移 P 后,原 P 池中的对象无法自动带过去,可能造成复用率下降
- 没有全局池视角,无法统计当前总复用数或强制清空
性能提示:对短生命周期、高频小对象(如日志行 buffer),本地池收益明显;对长周期或跨 P 频繁传递的对象,收益递减甚至为负。
GC 周期清理不可控,不适合保存状态或连接
sync.Pool 的对象在每次 GC 启动前被标记为“可回收”,并在 STW 阶段统一清除——你无法预测哪次 GC 会清掉哪个对象,也无法注册析构回调。
容易踩的坑:
- 把数据库连接、HTTP client、加密上下文等放进去:它们有状态、需显式 Close,GC 清理会导致资源泄漏或 panic
- 依赖池中对象保持某种初始化状态(如已 set TLS config):下次
Get()可能拿到刚 New 出来的干净实例 - 在
init()或包级变量中预热池子:无意义,因为预热对象大概率在首次 GC 时就被清掉
真正适合的类型:纯内存、无外部依赖、构造开销大、生命周期短、可安全 Reset 的对象,比如 json.Encoder、临时 sync.Map、解析中间结构体。
复杂点在于:复用收益和 GC 行为强耦合,压测时表现波动大;上线后若 GC 频率突增(如内存压力上升),池命中率可能断崖下跌——这点常被忽略。










