Go 性能实战:用 sync.Pool 复用对象把 GC 压力降下来

test12026-09-251 次阅读

问题从哪来

高并发服务里,如果每处理一个请求都 new 一个临时大对象(比如字节缓冲、序列化中间结构),这些对象很快变成垃圾,GC 频率随之升高,STW 和 CPU 占用都上去了。sync.Pool 提供了一个进程级的临时对象池,让对象用完后归还、下次复用,从而削减分配次数。

最小可用示例

package main

import (
	"bytes"
	"sync"
)

var bufPool = sync.Pool{
	New: func() any {
		return new(bytes.Buffer)
	},
}

func handle(data []byte) []byte {
	buf := bufPool.Get().(*bytes.Buffer)
	buf.Reset() // 关键:复用前清空,别带着上次的数据
	buf.Write(data)
	out := append([]byte(nil), buf.Bytes()...)
	bufPool.Put(buf) // 用完归还
	return out
}

配合 bytes.Buffer 做 JSON 批量序列化

下面这个例子把多个结构体编码进一个缓冲,避免反复分配:

func encodeBatch(items []Item) ([]byte, error) {
	buf := bufPool.Get().(*bytes.Buffer)
	buf.Reset()
	defer func() { bufPool.Put(buf) }()

	enc := json.NewEncoder(buf)
	for _, it := range items {
		if err := enc.Encode(it); err != nil {
			return nil, err
		}
	}
	return append([]byte(nil), buf.Bytes()...), nil
}

使用准则

  • 池化的对象必须 Reset 后才能再用,否则会混入上一次请求的脏数据,这是最常见的 bug。
  • Put 回去的对象可能被任意时刻被 GC 清掉,所以它适合"临时、可重建"的对象,不能当作缓存或状态容器。
  • New 函数要轻量,只在池空时才会调用;池满时 Put 的对象会被直接丢弃。
  • 对象大小差异很大时,可以建多个按尺寸分级的池,避免小对象占用大缓冲。

如何验证收益

用 go 自带的基准测试对比有无 Pool 的分配次数:

func BenchmarkWithPool(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_ = handle(make([]byte, 1024))
	}
}

跑 go test -bench=. -benchmem,重点看 allocs/op 这一列:引入 Pool 后,单次的堆分配数通常能降到接近 0。再结合线上 pprof 的 alloc_space 指标,就能确认 GC 压力是否真的下来了。

T

test1

文章作者

问题从哪来 高并发服务里,如果每处理一个请求都 new 一个临时大对象(比如字节缓冲、序列化中间结构),这些对象很快变...

分类
技术
发布时间
2026-09-25
字数
约 1298 字
阅读
1 次