sync.Pool 与对象复用
问题
sync.Pool 是什么?怎么用它减少 GC 压力?
答案
sync.Pool 原理
sync.Pool 是一个临时对象池,用于复用频繁创建/销毁的临时对象,减少堆分配和 GC 压力。
核心特点:
- 不是持久缓存:池中对象可能在 GC 时被清理
- 并发安全:内部使用 per-P 本地缓存 + victim cache
- 零值可用:但通常需要设置
New函数
基本用法
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func processRequest(data []byte) string {
// 从池中获取对象
buf := bufPool.Get().(*bytes.Buffer)
// 用完后归还(重要:先重置再归还)
defer func() {
buf.Reset() // 清空内容,避免数据泄漏
bufPool.Put(buf)
}()
buf.Write(data)
return buf.String()
}
实际使用场景
1. JSON 编解码
var encoderPool = sync.Pool{
New: func() any {
return json.NewEncoder(io.Discard)
},
}
2. HTTP Handler 中复用 buffer
var bufPool = sync.Pool{
New: func() any {
return make([]byte, 0, 4096)
},
}
func handler(w http.ResponseWriter, r *http.Request) {
buf := bufPool.Get().([]byte)[:0] // 取出并重置长度
defer bufPool.Put(buf)
// 使用 buf 处理请求...
}
3. 标准库中的使用
fmt 包内部使用 sync.Pool 复用 pp 结构体:
// fmt 包源码(简化)
var ppFree = sync.Pool{
New: func() any { return new(pp) },
}
func Fprintf(w io.Writer, format string, a ...any) (n int, err error) {
p := ppFree.Get().(*pp)
p.doPrintf(format, a)
n, err = w.Write(p.buf)
p.free() // 内部调用 ppFree.Put(p)
return
}
性能对比
// Benchmark 对比
func BenchmarkWithoutPool(b *testing.B) {
for i := 0; i < b.N; i++ {
buf := new(bytes.Buffer) // 每次分配新对象
buf.WriteString("hello")
_ = buf.String()
}
}
func BenchmarkWithPool(b *testing.B) {
pool := sync.Pool{New: func() any { return new(bytes.Buffer) }}
for i := 0; i < b.N; i++ {
buf := pool.Get().(*bytes.Buffer)
buf.Reset()
buf.WriteString("hello")
_ = buf.String()
pool.Put(buf)
}
}
// 结果示例
// BenchmarkWithoutPool-8 5000000 320 ns/op 112 B/op 2 allocs/op
// BenchmarkWithPool-8 10000000 120 ns/op 0 B/op 0 allocs/op
注意事项
sync.Pool 使用注意
- Pool 中的对象可能被 GC 清理——不能依赖对象持久存在
- 归还前必须重置对象状态——避免数据泄漏和状态污染
- Pool 不是连接池——连接池需要持久管理,用专门的连接池实现
- 不要 Pool 大对象——大对象复用收益低,反而占用额外内存
- Pool 的 New 可以为 nil——此时 Get 返回 nil,需要自己处理
常见面试问题
Q1: sync.Pool 的对象什么时候被清理?
答案:
每次 GC 时都可能被清理。Go 1.13 引入了 victim cache 机制:
- 第一次 GC:将 Pool 中的对象移到 victim cache
- 第二次 GC:清理 victim cache
这给了对象"第二次机会",减少了 GC 后的性能抖动。
Q2: sync.Pool 为什么不适合做连接池?
答案:
因为 Pool 中的对象随时可能被 GC 清理。数据库连接/TCP 连接是宝贵资源,不能说清就清。连接池需要:
- 最小连接数保障
- 连接健康检查
- 超时管理
应使用专门的连接池实现(如 database/sql 内置的连接池)。
Q3: 什么时候应该使用 sync.Pool?
答案:
满足以下条件时适合用 Pool:
- 对象创建成本较高(如
bytes.Buffer、大 slice) - 对象使用频率高(如每次 HTTP 请求都用)
- 对象是临时的(用完就不需要了)
- 对象可以安全地 Reset 后复用