适合放进 sync.Pool 的是热路径中高频创建、生命周期短、结构稳定且不持有外部引用的临时对象,如 bytes.Buffer、ParseCtx、需重置的 JSON Decoder 等;含 map/chan/指针的对象易致泄漏或竞态。

热路径里什么对象适合放进 sync.Pool
不是所有对象都值得复用。真正该放进去的,是那些在热路径中高频创建、生命周期短、结构稳定、且不持有外部引用的临时对象。比如 HTTP 中的 bytes.Buffer、自定义的解析上下文 ParseCtx、JSON 解码用的 Decoder 实例(需重置)、或小尺寸的切片封装结构体。如果对象里嵌了 map、chan 或指针指向长生命周期数据,放进 sync.Pool 反而可能引发内存泄漏或竞态——因为 Get() 拿到的可能是之前 goroutine 留下的“脏”状态。
sync.Pool 的 New 函数必须返回干净对象
很多人只写 New: func() interface{} { return &MyStruct{} },但这是错的起点。如果 MyStruct 有可变字段(如 buf []byte、fields map[string]string),必须在 New 里做初始化,或者更推荐:在每次 Get() 后手动重置。例如:
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
// 使用时:
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset() // 必须调用!否则残留上次写入内容
buf.WriteString("hello")
// ... use buf
bufPool.Put(buf)
漏掉 Reset() 是最常见 bug,会导致上一次请求的数据污染下一次响应。
Put 前必须确保对象不再被其他 goroutine 引用
sync.Pool 不做所有权检查,Put() 只是把指针加进本地池队列。如果对象还在被 channel 发送、被闭包捕获、或正被另一个 goroutine 并发读写,Put() 后它可能被别的 goroutine Get() 到并误用。典型翻车场景:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 把 HTTP handler 里的
req.Context()关联对象Put(),但中间件还在异步用它 - 在 goroutine 里
Put()后又继续用该对象(尤其是 defer Put 场景) - 对象含
sync.Mutex,Put()后未保证已解锁,下次Get()直接死锁
原则就一条:Put 前,这个对象的生命周期必须彻底结束,且无任何活跃引用。
注意 GC 时机和 Pool 的局部性影响性能
sync.Pool 是 per-P 的,没有跨 P 共享;GC 会清空所有池(包括未被使用的干净对象)。所以别指望它替代全局对象池或做长期缓存。压测时你会发现:QPS 上去后,池命中率(sync.Pool 的 Get() 返回非 New 创建对象的比例)通常在 70%–95% 之间浮动,但一旦触发 STW,瞬间归零。如果你的热路径对象构造开销不大(比如只是几个字段赋值),用 sync.Pool 可能得不偿失——函数调用和接口转换的开销反而更重。建议先用 go tool pprof 确认堆分配热点,再决定是否引入。
真正难处理的是对象状态重置逻辑的完整性:Buffer 要 Reset(),带字段的结构体要逐个清零,slice 要 buf[:0] 而不是直接 nil——这些细节没全覆盖,复用就等于埋雷。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










