sync.pool仅对极少数场景有效,误用会导致rss升高、gc变慢、bug难查;不可池化的对象包括*sql.db等资源型、小对象、含sync.mutex的结构体及大结构体;new是兜底工厂而非构造函数,需返回全新对象;get后必须手动reset;put表示放弃所有权,需严守作用域避免竞争。

别急着加 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 居高不下
sync.Pool.New 不是构造函数,是兜底工厂
New 只在 Get() 发现池空时才调,且可能被多个 goroutine 并发调多次——它不控制对象来源,也不保证每次 Get() 都触发它。
- 错误写法:
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)} }(每次返回全新对象,容量固定防逃逸)
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{},后者会逃逸) - 对
map[string]interface{}:不能只m = make(map[string]interface{}),要遍历delete()或复用底层数组
Put() 是放弃所有权,不是“归还”
一旦 Put(),该对象可能被任意 goroutine 下次 Get() 拿走。如果此时还有 goroutine 正在读写它,就会引发数据竞争。
- 危险操作:
Put()一个正被json.Unmarshal写入的*User,而解析还没结束 - 安全边界:严格限定作用域,比如只在单个 HTTP handler 内完成
Get → use → Put - 避免
defer pool.Put(x):panic 或提前return会导致漏调,对象永久泄漏 - 切片类对象
Put前建议截断长度:pool.Put(buf[:0]),保留底层数组但清空逻辑长度
最常被忽略的一点:Pool 中的对象最多活过一次 GC 周期,且每个 P 有独立副本。跨 goroutine、跨请求、跨 handler 的复用逻辑,基本都失效。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











