sync.pool 并非万能加速器,仅对高频创建、短生命周期、大小稳定的对象(如 bytes.buffer)有效;误用会导致 gc 变慢、rss 升高、脏数据和偶发 bug,必须严格遵循 get 后 reset、put 前确保无引用、new 仅兜底且幂等三大原则。

sync.Pool 不是“用了就快”,它只对特定对象有效;加错反而让 GC 更慢、内存 RSS 升高、bug 偶发难复现。
Get 后不调 Reset 就会脏数据,不是 bug 是设计使然
sync.Pool 只存指针、不清理字段。它把上一个 goroutine 用过的 bytes.Buffer、json.Decoder 或自定义结构体原样给你,len/cap/Err/map 内容全保留。
常见现象:
- bytes.Buffer.String() 返回前一次拼接的残留内容
- json.Unmarshal 解析出旧 key 或 panic “invalid character”
- http.Request.Header 里混着上个请求的 Cookie
必须手动重置,且 Reset 要幂等:
-
buf.Reset()(bytes.Buffer) -
decoder.DisallowUnknownFields()(json.Decoder) -
ctx.Input = ctx.Input[:0](切片字段) -
ctx.Err = nil(错误字段) -
for k := range ctx.cache { delete(ctx.cache, k) }(map 字段)
别信“New 已初始化就不用 Reset”——New 只兜底,Get 返回的 99% 是别人用过的脏对象。
New 函数写错直接导致 panic 或循环污染
New 是懒加载工厂,不是构造入口。它只在池空时触发,且可能被多个 goroutine 并发调用多次。
错误写法:
- return pool.Get() → 循环引用,panic: “sync: inconsistent pool behavior”
- return &globalBuf → 所有 goroutine 共享同一实例,竞态爆炸
- log.Println("created"); return new(bytes.Buffer) → 日志副作用,调用时机不可控
正确写法要同时满足:
- 每次返回全新对象(不能复用池内已有对象)
- 返回对象字段全零值,或内部已显式调用 Reset
例如:return &MyStruct{Data: make([]byte, 0, 512)},固定 cap 防逃逸,Data切片 len=0、cap=512、底层数组可复用。
哪些对象根本不该放进 sync.Pool
不是“能复用”就该池化,放错等于主动埋雷: -*sql.DB、http.Client、os.File:自带资源管理逻辑,Put 不会 Close
- string、int、小 struct:Go 分配器对小对象极高效,sync.Pool 的原子操作开销反而更高
- 含 sync.Mutex 的结构体:Pool 不调 Unlock(),下次 Get 直接 panic
- 大结构体(> 1KB)或底层数组频繁扩容的 []byte:池内碎片堆积,GC 扫不掉,RSS 居高不下
真正适合的只有三类:
- 高频创建 + 短生命周期 + 大小稳定(如
bytes.Buffer、cap 固定的[]byte) - 创建开销大且可安全重置(如
json.Decoder、带预分配 buffer 的解析器) - 无外部资源、无 finalizer、无跨 goroutine 长期持有需求
Put 前没确保对象不再被其他 goroutine 使用
sync.Pool.Put 是裸指针移交,不检查引用计数、不阻塞、不等待。
典型踩坑:
- 在 goroutine 中启动异步处理,然后立刻 Put 对象 → 异步逻辑读到已复用的内存,数据错乱
- HTTP handler 里 Put 了 req,但中间件还在读 req.Header → 读到别人刚 Put 进来的脏 Header
安全做法:
-
Put必须发生在所有对该对象的读写操作彻底结束之后 - 若对象被传入 goroutine,必须用 channel 或
sync.WaitGroup显式同步完成 - 对 HTTP 请求这类典型场景,
Put应放在 handler 最末尾,且确保所有中间件、defer、recover 已执行完毕
最隐蔽的问题永远藏在 Reset 是否彻底、Put 是否过早、New 是否真干净——这三个点漏掉任何一个,性能没提上来,bug 先上线了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











