---
title: "sync.Pool em Go: Reutilização de Objetos e Performance em Produção"
url: "https://golang.com.br/blog/sync-pool-go-reutilizacao-objetos-performance/"
markdown_url: "https://golang.com.br/blog/sync-pool-go-reutilizacao-objetos-performance.MD"
description: "Aprenda sync.Pool em Go para reutilizar objetos, reduzir alocações e a pressão do GC em serviços de alta vazão. Padrões para buffers, bytes.Buffer, protobuf e armadilhas de produção com benchmark."
date: "2026-06-21"
author: "Golang Brasil"
---

# sync.Pool em Go: Reutilização de Objetos e Performance em Produção

Aprenda sync.Pool em Go para reutilizar objetos, reduzir alocações e a pressão do GC em serviços de alta vazão. Padrões para buffers, bytes.Buffer, protobuf e armadilhas de produção com benchmark.


Alocações são baratas em Go, mas em serviços de alta vazão elas se acumulam: cada request cria buffers, slices temporários, structs de parse e objetos de protocolo que viram lixo milissegundos depois. O coletor de lixo limpa tudo, mas o custo de varrer o heap escala com o número de objetos vivos. Quando o perfil mostra que boa parte do tempo da CPU está em `runtime.gcBgMarkWorker` ou em `mallocgc`, a pergunta certa não é "como alocar mais rápido", mas "como alocar menos". É exatamente isso que `sync.Pool` resolve.

Este guia mostra como usar `sync.Pool` em serviços reais, quando ele ajuda de verdade e onde ele vira uma otimização prematura que adiciona complexidade sem retorno. Ele complementa o [guia de pprof em produção](/blog/pprof-go-producao/), o [guia de PGO (Profile-Guided Optimization)](/blog/go-pgo-profile-guided-optimization-performance/), o [guia de concorrência](/aprenda/concorrencia-go/) e os [padrões de concorrência em Go](/tutoriais/go-concurrency-patterns/).

## O que sync.Pool faz (e o que não faz)

`sync.Pool` é um conjunto de objetos temporários, seguros para uso concorrente, que podem ser guardados e reutilizados. A interface é mínima:

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

func handler(w http.ResponseWriter, r *http.Request) {
    buf := bufPool.Get().(*bytes.Buffer)
    defer bufPool.Put(buf)
    buf.Reset()

    // ... usa buf para montar a resposta ...
    w.Write(buf.Bytes())
}
```

`Get` devolve um objeto do pool (ou chama `New` se o pool estiver vazio). `Put` devolve o objeto para reutilização. A regra fundamental é: **objetos no pool podem ser descartados a qualquer momento, sem aviso**. O runtime esvazia o pool a cada coleta de lixo. Isso tem duas consequências importantes:

1. `sync.Pool` não é um cache. Nunca guarde objetos que você não pode reconstruir.
2. `sync.Pool` não é uma conexão de banco de dados nem um worker. Para isso use um pool real, como o de [pgxpool para PostgreSQL](/blog/pgxpool-go-postgresql-producao/) ou um [worker pool com fila de jobs](/blog/worker-pool-go-fila-jobs/).

## O caso clássico: buffers em handlers HTTP

O padrão mais comum e mais seguro é reutilizar `bytes.Buffer` em hot paths. Sem pool, cada serialização de JSON, cada montagem de corpo de resposta e cada encoding gera um buffer novo:

```go
func renderJSON(w http.ResponseWriter, v any) error {
    var buf bytes.Buffer
    if err := json.NewEncoder(&buf).Encode(v); err != nil {
        return err
    }
    w.Header().Set("Content-Type", "application/json")
    _, err = w.Write(buf.Bytes())
    return err
}
```

Isso é correto, mas em um endpoint que recebe 50 mil requisições por segundo o heap enche de buffers descartáveis. Com pool, a mesma função reutiliza memória:

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

func renderJSON(w http.ResponseWriter, v any) error {
    buf := jsonBufPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        jsonBufPool.Put(buf)
    }()

    if err := json.NewEncoder(buf).Encode(v); err != nil {
        return err
    }
    w.Header().Set("Content-Type", "application/json")
    _, err = w.Write(buf.Bytes())
    return err
}
```

Duas armadilhas sutis aqui. Primeiro, sempre chame `Reset()` — tanto no `Get` quanto no `Put`. Resetar no `Put` deixa o objeto pronto para o próximo uso; resetar no `Get` protege contra o caso (raro, mas possível) de um objeto que entrou no pool sem reset. Segundo, nunca retenha `buf.Bytes()` depois do `Put`. O conteúdo do slice subjacente pode ser sobrescrito pelo próximo usuário do objeto.

## Quando sync.Pool realmente ajuda

`sync.Pool` brilha em três cenários mensuráveis. O primeiro são hot paths de curta duração: handlers HTTP, renderização de templates, marshaling de protocolo, logs estruturados com [slog](/blog/slog-go-logging-estruturado/). O segundo são objetos caros de construir mas baratos de reutilizar — não "caros" no sentido de tempo de CPU, mas que alocam arrays internos grandes (buffers de 4 KiB, slices de capacidade conhecida). O terceiro são workloads com rajadas: quando a vazão varia muito, o pool absorve o pico sem crescer o heap permanentemente, porque o GC esvazia o pool quando a pressão passa.

O sinal de que vale a pena é sempre o perfil. Antes de adicionar `sync.Pool`, rode `go tool pprof` no seu serviço (veja o [guia de pprof](/blog/pprof-go-producao/)) e olhe o profile de alocação (`-alloc_objects`, `-alloc_space`). Se `bytes.Buffer.grow`, `runtime.makeslice` ou `encoding/json.(*encodeState).marshal` dominarem, o pool vai ajudar. Se o gargalo for CPU em código seu, ou latência de rede, ou locks em banco de dados, o pool é otimização errada — isso é exatamente o que torna [PGO](/blog/go-pgo-profile-guided-optimization-performance/) e a leitura de flame graphs mais valiosos que palpites.

## Benchmark prova o ponto

Benchmark é a única forma honesta de validar a mudança. Compare antes e depois:

```go
func BenchmarkRenderSemPool(b *testing.B) {
    var w io.Discard
    payload := map[string]any{"id": 42, "nome": "Diego", "itens": []int{1, 2, 3, 4, 5}}
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        _ = renderJSONSemPool(w, payload)
    }
}

func BenchmarkRenderComPool(b *testing.B) {
    var w io.Discard
    payload := map[string]any{"id": 42, "nome": "Diego", "itens": []int{1, 2, 3, 4, 5}}
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        _ = renderJSONComPool(w, payload)
    }
}
```

Um resultado típico mostra o pool cortando alocações por operação de ~12 para ~3 e reduzindo `B/op` de ~1,5 KiB para ~150 bytes. A diferença em nanossegundos por operação pode parecer pequena isoladamente, mas multiplicada por milhões de requests vira dezenas de por cento de CPU devolvida. Sempre meça no seu workload real — números de benchmark de blog alheio não valem nada.

## Padrões avançados: pré-alocação no New

O construtor `New` pode devolver um objeto já dimensionado para o tamanho típico do seu uso, eliminando o primeiro `grow`:

```go
var headerPool = sync.Pool{
    New: func() any {
        b := make([]byte, 0, 256)
        return &b
    },
}

func writeHeader(dst io.Writer, headers map[string]string) error {
    bp := headerPool.Get().(*[]byte)
    defer func() {
        *bp = (*bp)[:0]
        headerPool.Put(bp)
    }()

    buf := *bp
    for k, v := range headers {
        buf = append(buf, k...)
        buf = append(buf, ": "...)
        buf = append(buf, v...)
        buf = append(buf, '\r', '\n')
    }
    buf = append(buf, '\r', '\n')
    _, err := dst.Write(buf)
    return err
}
```

Note três detalhes que separam código production-grade de tutorial. Guardamos `*[]byte` (ponteiro para slice) e não `[]byte`, porque `Put` recebe `any` e devolver um slice diretamente faz o escape para o heap. Resetamos com `*bp = (*bp)[:0]` para preservar a capacidade. E nunca passamos `buf` para nenhuma goroutine que sobreviva ao `defer` — após o `Put`, o slice pode ser reusado por outra goroutine e seus dados sobrescritos. Esses detalhes são a diferença entre um pool que acelera o sistema e um pool que introduz [data races](/erros/race-condition-data-race/) silenciosos.

## Armadilhas que invalidam o pool

A primeira armadilha é guardar estado no objeto. Se o objeto tem campos que carregam informação entre operações (IDs, contadores, referências a conexões), reusá-lo via pool vira bug intermitente. A segunda é esquecer o `Reset` e acabar acumulando dados de requisições anteriores — um vazamento sutil de informação entre usuários, grave em APIs multi-tenant. A terceira é usar pool para objetos grandes e raros: o GC vai coletar antes da próxima reutilização e você só adicionou overhead. A quarta, a pior, é tratar o pool como solução para um bug de vazamento de memória — se o heap cresce, o problema é provavelmente uma referência retida, não a falta de pool. Rode pprof no modo `-inuse_space` antes de otimizar.

## Pool em pipelines com concorrência

Em pipelines que combinam [errgroup](/blog/errgroup-go-concorrencia-cancelamento-erros/) ou [worker pools](/blog/worker-pool-go-fila-jobs/), o pool precisa respeitar o cancelamento de contexto. O padrão seguro é obter o objeto por unidade de trabalho e devolvê-lo no fim de cada unidade, nunca no fim do pipeline inteiro:

```go
func processar(ctx context.Context, in <-chan Registro) error {
    g, ctx := errgroup.WithContext(ctx)
    for w := 0; w < runtime.NumCPU(); w++ {
        g.Go(func() error {
            for reg := range in {
                if err := ctx.Err(); err != nil {
                    return err
                }
                if err := transformar(ctx, reg); err != nil {
                    return fmt.Errorf("registro %d: %w", reg.ID, err)
                }
            }
            return nil
        })
    }
    return g.Wait()
}

func transformar(ctx context.Context, reg Registro) error {
    buf := bufPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufPool.Put(buf)
    }()
    // ... serializa, valida, envia ...
    return nil
}
```

O detalhe decisivo é o `defer` dentro de `transformar`: cada registro pega e devolve seu buffer. Se você pegasse o buffer fora do loop do worker e só devolvesse no fim, um único objeto acumularia o trabalho de milhares de registros e nunca seria reusado — exatamente o oposto do objetivo. Em pipelines com [cancelamento por contexto](/blog/context-timeout-cancelamento-go/), o `defer` também garante que o buffer volte ao pool mesmo quando a função retorna cedo por erro.

## Boas práticas resumidas

- Use `sync.Pool` apenas em hot paths identificados por profile de alocação, nunca por intuição.
- Sempre `Reset()` o objeto no `Put` e, por segurança, no `Get` também.
- Guarde ponteiros (`*bytes.Buffer`, `*[]byte`) para evitar escape para o heap.
- Nunca retenha slices derivados do objeto (`buf.Bytes()`, sub-slices) depois do `Put`.
- Dimensione o objeto no `New` com `make(..., 0, n)` quando o tamanho típico é conhecido.
- Não use pool para objetos com estado lógico, conexões, ou para "consertar" vazamentos.
- Re-meça com benchmark antes e depois; sem número, é otimização por fé.

`sync.Pool` é uma das ferramentas mais baratas da biblioteca padrão para cortar alocações em serviços de alta vazão, mas só paga quando o profile confirma que alocações são o problema. Ele não substitui arquitetura, não salva código mal escrito e não compensa um [pool de conexões mal configurado](/blog/pgxpool-go-postgresql-producao/). O que ele faz é devolver CPU e memória que estavam sendo desperdiçadas com objetos efêmeros — e em escala, isso é diferença entre um serviço que aguenta pico e um serviço que degrada. Frameworks de pooling variam entre stacks, mas o conceito de reutilizar buffers em hot paths é universal — vale comparar com a abordagem do <a href="https://python.dev.br/blog/golang-erros/" target="_blank" rel="noopener noreferrer" onclick="umami.track(portfolio-site-click, { destination: python.dev.br })">Python Dev Brasil sobre performance em backends</a> ao planejar otimização entre linguagens.
