sync.Pool仅适用于高频创建、短期存活、结构体大小适中的场景;不适用生命周期长、分配开销低、已自带复用机制或含长期指针的对象,且New必须返回全新对象,Put前须确保无任何goroutine再使用该对象。

sync.Pool 什么时候该用,什么时候别硬套
它不是万能加速器,只在「高频创建+短期存活+结构体大小适中」的场景下才值得引入。比如 HTTP 中间件里反复分配 *bytes.Buffer 或自定义请求上下文结构体;但如果你的对象生命周期长(如全局配置缓存)、或单次分配开销极低(如几个 int 字段的 struct),加 sync.Pool 反而增加调度开销和内存碎片。
- 典型适用:每次请求都 new 一个
json.Decoder、bufio.Reader、或含 slice 字段的临时 DTO - 明显不适用:持有锁、包含指针指向长期存活对象、或本身已复用(如
http.Request自带复用机制) - 注意:
sync.Pool不保证对象一定被复用——GC 时会清空,且无 LRU 等淘汰策略,纯靠「最近用过就留着」
New 字段必须返回全新对象,不能返回共享实例
这是最容易踩的坑:sync.Pool 的 New 函数如果返回了同一个全局变量或复用的底层数组,多个 goroutine 并发 Get/.Put 就会相互污染数据,引发隐蔽的竞态或 panic。
- 错误写法:
New: func() interface{} { return &myGlobalBuf }—— 全局变量被所有 goroutine 共享 - 正确写法:
New: func() interface{} { return &MyStruct{Data: make([]byte, 0, 128)} }—— 每次 New 都分配新内存 - 尤其注意 slice 和 map:不要直接
return make([]int, 0)后不清空内容,Put 前必须重置长度(s = s[:0]),否则下次 Get 到的是残留数据
Put 之前必须确保对象不再被任何 goroutine 使用
sync.Pool.Put 不做引用计数,也不检测是否还在被使用。一旦 Put 进去,Pool 可能在任意时刻把它交给别的 goroutine,此时原 goroutine 若继续读写该对象,就是典型的 use-after-free。
- 常见错误:在 defer 里 Put,但函数返回后仍有 goroutine 持有该对象指针(如启动了异步 callback)
- 安全做法:Put 前确认对象作用域已结束——通常放在处理逻辑末尾、且无 channel 发送、无闭包捕获、无 goroutine 启动
- 调试技巧:开启
GODEBUG=gctrace=1观察 GC 频次变化;配合-race检测潜在竞态
Go 1.19+ 的 Pool 改动影响实际复用率
从 Go 1.19 开始,sync.Pool 的本地池(per-P)清理时机更激进:当 P 被长时间闲置(如大量 goroutine 阻塞),其绑定的 pool 会被提前清空。这意味着高并发短任务(如大量快速 HTTP 请求)复用率可能比旧版本更低。
立即学习“go语言免费学习笔记(深入)”;
- 表现现象:压测时 GC 次数下降不明显,
runtime.ReadMemStats显示PauseNs改善有限 - 应对方式:避免过度依赖 Pool 复用,优先考虑栈上分配或对象内联;对关键路径可结合
pprof对比allocs和heap_inuse - 兼容提示:Go 1.21 引入
Pool.New可为 nil,但行为不变——仍需保证返回值是新对象
真正难的不是写对 Get 和 Put,而是判断某个对象到底「值不值得放进 Pool」。它要求你同时看清分配频率、对象生命周期、GC 日志里的 pause 分布,以及线上真实流量下的内存 profile —— 这些没法靠模板解决。











