用 sync.Pool 把短命对象的 GC 压力降到忽略不计
2026-05-18性能约 4 分钟
在高 QPS 的服务里,每秒分配数十万个临时 []byte 或 bytes.Buffer
会让 GC 把 P99 延迟拖出长尾。sync.Pool 提供了一个 per-P 的本地缓存,
让短命对象在多次请求间复用,从而几乎完全消除这部分分配。
var bufPool = sync.Pool{
New: func() any {
// 预分配一段合理容量,避免后续频繁扩容
return bytes.NewBuffer(make([]byte, 0, 4096))
},
}
func Render(w io.Writer, data *Payload) error {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset() // 关键:归还前不需要 Reset,但取出后必须
defer bufPool.Put(buf)
if err := tmpl.Execute(buf, data); err != nil {
return err
}
_, err := buf.WriteTo(w)
return err
}
三个真正的坑:第一,Pool 里的对象在每轮 GC 之间会被清空,所以它只适合
短命、可重建 的对象;不要拿它当连接池。第二,取出来一定要 Reset(),
否则会拿到上一个调用方的脏数据——这是真实生产事故的常见来源。第三,不要 Put
超大缓冲,否则会让 Pool 退化成一个内存膨胀器:在归还前判断
buf.Cap() > maxReusable 直接丢弃即可。
errgroup + context:让并发任务的失败传播变得"自然"
2026-05-21并发约 5 分钟
手写 sync.WaitGroup 拼并发,遇到任意一个子任务失败就要全员取消的场景,
几乎每次都会写错——要么漏掉 cancel(),要么 error 被覆盖。
golang.org/x/sync/errgroup 把这套模式封装成了三行代码。
func fetchAll(ctx context.Context, urls []string) ([][]byte, error) {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(8) // 控制并发度,避免压垮下游
results := make([][]byte, len(urls))
for i, url := range urls {
i, url := i, url // Go 1.22 前必须捕获
g.Go(func() error {
body, err := getWithCtx(ctx, url)
if err != nil {
return fmt.Errorf("fetch %s: %w", url, err)
}
results[i] = body
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err // 第一个返回 err 的任务胜出,其余被 ctx 取消
}
return results, nil
}
关键点在 WithContext 返回的 ctx:一旦任意 Go 内函数返回
非 nil 错误,这个 ctx 会立刻被取消,其它仍在跑的子任务通过 ctx.Done() 自动收到信号,
从而退出而不是继续浪费下游资源。配合 SetLimit 可以省掉手写 semaphore 的样板代码。
要注意 g.Wait() 只返回第一个非 nil 错误,若需要聚合所有失败,请用
errors.Join 在外层累计。