
sync.Pool 适合存什么类型的数据
它只适合存「可复用、无状态、生命周期由 Pool 管理」的对象。比如 *bytes.Buffer、*sync.Mutex(注意不是裸的 sync.Mutex)、自定义结构体指针。不能存含 goroutine 局部状态的值(如带 channel 字段且已初始化的 struct),也不能存需要精确控制释放时机的资源(如文件句柄、数据库连接)。
常见错误现象:sync.Pool 回收后,对象里的字段仍残留上次使用时的值,导致逻辑错乱——这是因为 Pool 不清零,只负责“拿”和“放”,清零得自己做。
- 每次从
Pool.Get()拿到的对象,必须手动重置关键字段(比如切片要buf = buf[:0]) - 避免存接口类型(如
interface{})包裹的指针,容易掩盖底层类型不一致问题 - 如果对象构造开销不大(比如只是几个 int 字段),用 Pool 反而增加 GC 压力,得不偿失
Get/ Put 必须成对出现,且不能跨 goroutine 使用
sync.Pool 的设计是 per-P(goroutine 绑定)缓存 + 全局共享后备,Put 进去的对象不一定能被同一个 goroutine 的下一次 Get 拿到,但绝不能假设它会“自动传播”到别的 goroutine。更危险的是:在 A goroutine Put 一个对象,B goroutine Get 到它后继续用,若 A 已经认为该对象“还回去了”并修改了它的内部状态,就产生竞态。
使用场景:典型的是 HTTP handler 中反复分配临时 buffer 或中间结构体,每个请求生命周期内 Get → 用 → Put。
立即学习“go语言免费学习笔记(深入)”;
- Put 前确保对象不再被当前 goroutine 引用(尤其是闭包、定时器回调里持有对象时)
- 不要在 defer 中无条件 Put —— 如果 Get 返回 nil(比如刚启动时池空),Put(nil) 是合法但无意义;更糟的是,如果对象已被其他地方复用,defer Put 会把它二次放回池,引发不可预测行为
- 禁止把
Pool.Get()结果传给另一个 goroutine 处理后再 Put —— 这等于跨 P 使用,破坏本地缓存语义
New 字段不是构造函数,而是兜底工厂
sync.Pool 的 New 字段只在 Get() 发现池空时调用一次,返回一个新对象。它不是每次 Get 都触发,也不是“初始化钩子”。很多人误以为在这里做资源预热或统计,结果发现 New 函数调用次数远低于预期。
性能影响:New 函数若耗时长(比如打开文件、发起网络请求),会拖慢首次 Get,而且它运行在调用 Get 的 goroutine 上,可能阻塞业务逻辑。
- New 应该极快,最好只是字面量构造或
&Struct{} - 不要在 New 里做任何同步操作(如加锁、channel send)
- 如果想监控池命中率,别依赖 New 调用次数——应该自己包装 Get/Put,统计空池 Get 次数 vs 总 Get 次数
sync.Pool 没有大小限制,也不会主动清理内存
它不提供 SetMaxSize、Trim 或类似方法。Go 运行时会在每轮 GC 前扫描所有 Pool,把其中未被引用的对象批量清理掉。这意味着:对象可能在 GC 前一直驻留内存,也可能在某次 GC 后突然全空。
容易踩的坑:有人用 Pool 缓存大对象(如 MB 级 slice),又没控制总量,结果内存持续上涨直到下一次 GC;或者依赖“池里总有旧对象可用”,结果 GC 后第一次 Get 就走 New,延迟毛刺明显。
- 大对象慎用 Pool —— 优先考虑复用底层 byte slice(如
bytes.Buffer的Grow)而非整个结构体 - 不要用 Pool 替代内存池(memory pool)或对象池(object pool)的严格容量控制逻辑
- 压测时观察
GODEBUG=gctrace=1输出中的pool sweeps行,确认 GC 是否真的回收了 Pool 对象
Pool 的边界很清晰:它不是万能缓存,也不保证存在性,只在“高频创建销毁同类小对象”且“能接受非确定性生命周期”的场景下才真正省事。一旦需求里出现“必须保留 N 个”“超时自动释放”“按 key 区分复用”,就该换方案了。










