sync.Pool仅适用于高频创建、短生命周期、大小稳定且可安全重置的临时对象;禁用于数据库连接、小基础类型、含锁/外部资源/大结构体的对象,New是兜底工厂而非构造器,Get后必须手动Reset,Put前须确保无其他goroutine使用。

别急着加 sync.Pool,它只对极少数场景真正有效;加错反而让 RSS 升高、GC 更慢、bug 更难查。
哪些对象能放进 sync.Pool?
不是“能复用”就该池化。以下类型放进池里,等于主动给 GC 和调试埋雷:
-
*sql.DB、http.Client、os.File:它们自带连接/资源管理逻辑,Pool不负责Close,也不感知生命周期 -
string、int、struct{ ID int; Name string }:Go 分配器对小对象极高效,Pool的原子操作开销反而更高 - 含
sync.Mutex或未导出字段的结构体:Pool不调Lock()也不清锁状态,Put前若已Lock,下次Get就 panic - 大结构体(> 1KB)或底层数组频繁扩容的
[]byte:池内碎片堆积,GC 扫不掉,RSS 居高不下
真正适合的只有三类:
- 高频创建 + 短生命周期 + 大小稳定(如
bytes.Buffer、固定 cap 的[]byte) - 创建开销大且可安全重置(如
json.Decoder、自定义解析器结构体) - 无外部资源、无 finalizer、无跨 goroutine 长期持有需求
sync.Pool.New 是兜底工厂,不是构造函数
New 只在 Get() 发现池空时才调,且可能被多个 goroutine 并发调多次——它不控制对象来源,也不保证每次 Get() 都触发它。
立即学习“go语言免费学习笔记(深入)”;
- 错误写法:
New: func() interface{} { return &globalBuf }(返回全局变量地址,所有 goroutine 共享同一实例) - 错误写法:
New: func() interface{} { log.Println("created"); return new(bytes.Buffer) }(日志副作用,调用时机不可控) - 正确写法:
New: func() interface{} { return &MyStruct{Data: make([]byte, 0, 512)} }(每次返回全新对象,容量固定防逃逸)
注意:New 返回的对象也必须是“干净”的——字段全零值,或显式初始化;否则第一次 Get 到的可能是带残留状态的脏对象。
Get() 后必须手动 Reset(),不能依赖 New
Get() 返回的可能是上次 Put() 进去的旧对象,字段值完全不可控。Go 绝不会帮你调 Reset(),也不会检查字段是否残留。
- 对
bytes.Buffer:必须显式调b.Reset(),不能只靠New返回新实例 - 对自定义结构体:必须提供幂等
Reset()方法,并在Get()后立刻调用 - 例如:
obj.ID = 0;obj.Data = obj.Data[:0];*obj.Cache = map[string]int{}(注意不是obj.Cache = map[string]int{},后者会逃逸)
漏掉 Reset() 是最隐蔽的 bug 来源:JSON 解析错乱、HTTP body 被截断、字段值污染下游逻辑——现象偶发、难以复现、堆栈无提示。
Put() 是放弃所有权,不是“归还”
一旦 Put(),该对象可能被任意 goroutine 下次 Get() 拿走。如果此时还有 goroutine 正在读写它,就会引发数据竞争。
- 危险操作:
Put()一个正被json.Unmarshal写入的*User,而解析还没结束 - 避免漏调:
defer pool.Put(x)看似干净,但 panic 或提前return会导致对象永久泄漏 - 更稳妥做法:在明确的、不会跳过的出口路径上
Put,比如 handler 处理完请求后、解析完一段数据后 - 禁止重复
Put:Go 1.19+ 对重复Put会直接 panic,务必检查是否已Put过
最难判断的从来不是怎么写 Get 和 Put,而是这个对象“内部有没有隐藏的、不可见的状态依赖”——它是否真的能被安全重置。这点没法靠工具检查,只能靠读源码、跑压测、看 pprof heap profile 里的对象存活链。



















