sync.pool 适合复用创建开销大、生命周期短、无状态或易重置的对象,如 bytes.buffer、sync.mutex(需重置)、自定义解析器;不适合含未清空字段或依赖外部状态的对象。

sync.Pool 适合复用什么类型的对象
它不是万能缓存,只适合「创建开销大 + 生命周期短 + 无状态或易重置」的对象。比如 *bytes.Buffer、*sync.Mutex(需调用 mutex.Lock() 前重置)、自定义的解析器结构体。不适合复用含未清空字段的实例,或依赖外部状态(如打开的文件描述符、数据库连接)的对象。
常见错误现象:sync.Pool 取出的对象残留上次使用数据,导致逻辑错乱或 panic;或者误把需要显式关闭的资源(如 http.Response.Body)丢进池里,造成泄漏。
- 必须在
New字段中返回「干净」对象,不能复用已初始化但未清理的实例 - 每次从池中取出后,手动重置关键字段(例如
buf.Reset()、parser.err = nil) - 避免放入包含指针、map、slice 等引用类型且未做深拷贝/清空的对象
Get/put 时机不对会导致对象泄漏或性能反降
池中对象不会自动销毁,也不保证复用率。如果 Put 被跳过(比如函数提前 return、panic 未 recover),对象就丢了;如果 Get 后长期不 Put(比如塞进全局 map 或 channel),等于内存泄漏。
典型场景:HTTP handler 中复用 json.Decoder,但忘了在 defer 里 Put;或在 goroutine 中获取后传给另一个长期运行的协程,导致对象滞留。
立即学习“go语言免费学习笔记(深入)”;
- 务必配对使用:
defer pool.Put(x)是最安全的习惯 - 不要在可能 panic 的路径外直接
Put,优先用 defer 包裹 - 避免跨 goroutine 传递池对象——不同 P 的本地池互不共享,跨协程传递会失去本地性优势
- 高并发下频繁
Get/Put小对象(如int或小 struct)反而比直接分配慢,因为原子操作和锁开销盖过了分配收益
sync.Pool 没有大小限制,GC 会批量清理
它不提供容量控制,也没有 LRU 或 TTL。每个 P(处理器)维护一个本地池,加上一个全局池。当 GC 触发时,所有本地池内容被清空,全局池保留部分对象供下次使用。
这意味着:你无法靠 sync.Pool 实现「最多缓存 N 个」的语义;也不能假设某个对象放进去就一定能被下次 Get 拿到;更不能依赖它做资源配额管理。
- 不要在
New函数里做耗时操作(如启动 goroutine、网络请求),GC 清理时会阻塞 - 如果对象初始化成本本身很高(比如加载配置文件),放进
sync.Pool可能得不偿失 - 观察实际复用率要用
runtime.ReadMemStats查MPoolSys和分配差异,不能只看代码写了Put
替代方案比 sync.Pool 更合适的情况
当对象需要严格生命周期控制、跨 goroutine 共享、或带上下文约束时,sync.Pool 往往是错的选择。比如连接池(database/sql.DB)、任务缓冲区(chan *Task)、或需要按 key 分桶复用的解析器(如不同协议头对应不同 parser)。
- 连接类资源用专用池:如
net/http.Transport自带连接复用,或用golang.org/x/net/http2的流控机制 - 需要 key 化复用?改用
map[string]*parser+sync.RWMutex,并配合定时清理 - 临时切片复用?优先考虑
make([]T, 0, cap)配合[:0]截断,比sync.Pool更轻量 - 测试时发现复用率低于 30%,大概率说明对象不够“重”,或者 Get/Put 路径不匹配
真正难的不是写对 sync.Pool 的用法,而是判断「这个对象到底值不值得塞进去」——它既不解决内存碎片,也不替代逃逸分析,更不承诺复用。用之前先跑压测,别凭感觉。











