Golang 开发技术博客

记录 Go 语言里那些值得反复咀嚼的高阶用法

用 sync.Pool 把短命对象的 GC 压力降到忽略不计

2026-05-18性能约 4 分钟

在高 QPS 的服务里,每秒分配数十万个临时 []bytebytes.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 在外层累计。