sync.pool对象在gc时被清空,go 1.13起经victim缓存延至第二次gc才丢弃;get返回nil属正常设计,需手动reset重置状态,不可用于资源管理,仅适用于高频、短生命周期、小对象。

sync.Pool 的对象会在 GC 时被清空
每次运行 runtime.GC()(包括自动触发的 GC 周期),sync.Pool 内部的 victim 缓存会被提升为当前池,而原 local 缓存则被丢弃——这意味着所有未被 Get 复用的对象,在本轮 GC 后就不可见了。这不是“泄漏”,而是设计使然。
这个机制直接导致:你不能靠 Put 进去就指望下次 Get 一定能拿到;也不能把需要跨 GC 周期存活的对象往里塞。常见错误是 Put 了一个带指针字段的 struct,没清零,下轮 Get 拿到时字段仍指向已回收内存,引发 panic 或数据错乱。
- GC 周期越短(比如
GOGC=20),池中对象“存活窗口”越窄,复用率越低 - 高并发短请求场景(如 HTTP handler)中,对象往往在一次 GC 前就被复用,收益明显
- 长周期任务(如后台 goroutine 持有对象数秒以上)基本拿不到复用,反而因 Put 带来锁开销
Get 返回 nil 是合法且必须检查的
sync.Pool.Get() 不保证返回非 nil 值——尤其当池刚初始化、或刚经历 GC 清理、或本地 P 的 private 和 shared 都为空时,它会直接调用 New 函数;但若 New 返回 nil,Get 就返回 nil。标准库里几乎没人这么写,但你自己实现的 New 可能出错(比如初始化失败、资源不足)。
不检查就直接类型断言或解引用,会导致 panic。典型错误写法:buf := pool.Get().(*bytes.Buffer) —— 应该先判空:
obj := pool.Get()
if obj == nil {
obj = new(MyStruct)
}
myObj := obj.(*MyStruct)
myObj.Reset() // 注意 Reset 必须在 Get 后立即调用
Put 前不清零字段等于埋雷
Put 的本质是“归还使用权”,不是“存档备份”。sync.Pool 不做深拷贝,也不重置状态。如果 Put 一个 *bytes.Buffer 而没调用 Reset(),下次 Get 拿到的 buffer 里可能还残留上次的字节;Put 一个含 map 字段的 struct 却没清空 map,下次 Get 后直接写入会覆盖旧数据甚至引发 concurrent map writes。
- 切片类对象:要重置
len为 0(不是 cap),避免后续 append 越界或复用旧数据 - 结构体:所有可变字段需归零,特别是指针、map、slice、channel
- 不要依赖
defer pool.Put(x)在函数退出时自动清理——万一函数 panic,defer 不执行,对象就“消失”了
sync.Pool 不适合大对象或长生命周期对象
Go 运行时对大于 32KB 的对象绕过 sync.Pool 直接走 mcache/mcentral 分配路径,Put 进去也大概率不会被 Get 到。同时,对象生命周期若超过几个 GC 周期(比如绑定到 request context 并随 handler 存活数秒),它大概率已在某次 GC 中被清理,Put 回池反而造成 goroutine 泄漏(对象被“偷走”却不归还)。
真正适合的只有三类:短期高频分配的小对象([]byte、net.Buffers)、无状态结构体指针(*http.Request 的轻量 wrapper)、可彻底 Reset 的缓冲器(*json.Decoder)。
容易被忽略的一点:哪怕对象本身很小,只要它间接持有大内存(比如 struct 里有个未清零的 map[string][]byte),也会拖慢 GC 扫描——因为 GC 必须遍历整个 reachable graph。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











