Micro-otimização sem medição é opinião. Em Go, o caminho idiomático para medir um trecho de código é o benchmark nativo do pacote testing: funções BenchmarkXxx(b *testing.B), o comando go test -bench e, quando a pergunta muda de “quanto custa?” para “onde custa?”, o pprof.
Este guia cobre o fluxo completo de benchmarks em Go em 2026: como escrever com testing.B e b.Loop, como ler ns/op, B/op e allocs/op, como isolar setup, como evitar dead-code elimination, como gerar perfis a partir do próprio benchmark e quais armadilhas distorcem resultado em laptops e em CI. Ele complementa testes em tabela, fuzzing nativo, sync.Pool, PGO, go tool trace e o guia de testes.
Resposta rápida
| Pergunta | Ferramenta |
|---|---|
| Quanto custa esta função por chamada? | go test -bench |
| Quantas alocações ela faz? | -benchmem ou b.ReportAllocs() |
| Onde a CPU/heap se concentra? | pprof (a partir do benchmark ou do serviço) |
| Quando a latência explode no tempo? | go tool trace / Flight Recorder |
| A implementação está correta sob entradas ruins? | fuzz + testes de tabela |
Regra prática: se você não consegue nomear a operação que está medindo (“marshal de Pedido com 3 itens”, “parse de JWT”, “encode JSON de resposta 2 KB”), ainda não está pronto para benchmark. Escreva o teste de correção primeiro; meça depois.
O menor benchmark possível
Coloque o arquivo no mesmo pacote (ou no pacote _test externo) e siga a convenção de nome:
package jsonutil_test
import (
"encoding/json"
"testing"
)
type Pedido struct {
ID string `json:"id"`
Total float64 `json:"total"`
Itens int `json:"itens"`
Status string `json:"status"`
}
func BenchmarkMarshalPedido(b *testing.B) {
p := Pedido{
ID: "ped-42",
Total: 199.90,
Itens: 3,
Status: "pago",
}
b.ReportAllocs()
for b.Loop() {
if _, err := json.Marshal(p); err != nil {
b.Fatal(err)
}
}
}
Rode:
go test -bench=BenchmarkMarshalPedido -benchmem ./internal/jsonutil
Saída típica:
BenchmarkMarshalPedido-8 2500000 480.2 ns/op 128 B/op 2 allocs/op
Leitura:
-8:GOMAXPROCSno momento da execução (aqui, 8).2500000: iterações que o runner executou.480.2 ns/op: tempo médio por operação.128 B/op: bytes alocados por operação.2 allocs/op: alocações por operação.
Em APIs Go de produção no Brasil — checkout, PIX, webhooks, BFF — alocação costuma ser o primeiro número útil. Menos allocs/op significa menos pressão de GC sob carga. O Green Tea GC e o runtime moderno ajudam, mas não substituem não alocar à toa.
b.N versus b.Loop
A API clássica ainda aparece em muito código:
func BenchmarkConcatClassico(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = "go" + "lang"
}
}
Funciona, mas tem armadilhas históricas: setup acidental dentro do loop, compilador eliminando trabalho sem efeito observável e dificuldade de expressar “reset timer depois do prepare”.
Desde o Go 1.24, a forma preferida é testing.B.Loop:
func BenchmarkConcatLoop(b *testing.B) {
for b.Loop() {
_ = "go" + "lang"
}
}
Com b.Loop():
- O runner controla quantas vezes o corpo executa.
- O timing fica mais robusto em relação a setup e finalização.
- Várias classes de dead-code elimination e de contagem errada de
b.Ndeixam de ser armadilhas manuais.
Se o seu módulo ainda está em versões anteriores, use b.N com disciplina: prepare dados antes do loop, chame b.ResetTimer() se o setup for caro e force o compilador a observar o resultado (veja adiante). Em 2026, para código novo, prefira b.Loop().
Setup caro, ResetTimer e StopTimer
Quando o custo de preparar dados é grande, não deixe isso contaminar o ns/op:
func BenchmarkParseGrande(b *testing.B) {
raw := gerarJSONGrande() // caro: fica fora da medição
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
if _, err := ParsePedido(raw); err != nil {
b.Fatal(err)
}
}
}
Padrões úteis:
b.ResetTimer(): zera o relógio depois do setup.b.StopTimer()/b.StartTimer(): pausa a medição no meio (por exemplo, para recriar um arquivo entre iterações).b.ReportAllocs(): equivale a sempre rodar com-benchmemnaquele benchmark.
Evite I/O de rede dentro do microbenchmark “puro”. Se a operação real é “HTTP + JSON + banco”, você está medindo um cenário, não uma função — e aí faz mais sentido um teste de integração com Testcontainers, métricas e pprof sob carga.
Sub-benchmarks: compare opções no mesmo arquivo
Use b.Run para comparar implementações sem espalhar cinco funções soltas:
func BenchmarkEncode(b *testing.B) {
payload := map[string]any{
"id": "abc",
"total": 10.5,
"active": true,
}
b.Run("encoding/json", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
if _, err := json.Marshal(payload); err != nil {
b.Fatal(err)
}
}
})
b.Run("prealloc-buffer", func(b *testing.B) {
b.ReportAllocs()
buf := make([]byte, 0, 256)
for b.Loop() {
buf = buf[:0]
var err error
buf, err = json.Marshal(payload) // exemplo didático; em produção use Encoder
if err != nil {
b.Fatal(err)
}
sink = buf
}
})
}
var sink []byte
Filtro:
go test -bench=BenchmarkEncode -benchmem ./...
go test -bench=BenchmarkEncode/encoding -benchmem ./...
Sub-benchmarks são o equivalente de testes em tabela para performance: mesma entrada, várias estratégias, um relatório comparável. Combine com o hábito de table-driven tests no restante da suíte.
Dead-code elimination: o benchmark que mente
O compilador de Go é agressivo. Se o resultado de uma função não é observado, o trabalho pode sumir do binário de teste e seu ns/op vira ficção:
// RUIM: o compilador pode eliminar Encode se o retorno não for usado
func BenchmarkEncodeRuim(b *testing.B) {
for b.Loop() {
Encode(pedido) // retorno ignorado
}
}
Formas de forçar observação:
var globalSink any
func BenchmarkEncodeBom(b *testing.B) {
var local any
for b.Loop() {
local = Encode(pedido)
}
globalSink = local
}
Ou, quando há erro:
for b.Loop() {
out, err := Encode(pedido)
if err != nil {
b.Fatal(err)
}
globalSink = out
}
Checklist anti-mentira:
- Use o retorno (atribuir a
sinkglobal ou verificar erro). - Não meça só o caminho que aloca se a versão “rápida” só parece rápida porque foi otimizada para zero.
- Rode mais de uma vez; desvio grande entre execuções é sinal de ambiente barulhento, não de vitória.
-count, -benchtime e estabilidade
Um único run no notebook com CPU em modo economia de energia não é evidência. Use:
go test -bench=BenchmarkMarshalPedido -benchmem -count=10 ./internal/jsonutil
Interprete a mediana e a dispersão, não só a melhor linha. Para cargas longos:
go test -bench=BenchmarkMarshalPedido -benchtime=3s -benchmem ./internal/jsonutil
Ou por iterações fixas:
go test -bench=BenchmarkMarshalPedido -benchtime=1000000x -benchmem ./internal/jsonutil
Dicas de ambiente:
- Feche o navegador com 40 abas e o Docker build paralelo.
- Prefira máquina plugada na tomada (sem thermal throttle agressivo de bateria).
- Em CI, trate benchmark como detector de regressão grosseira, não como medidor de nanossegundo absoluto entre runners diferentes.
- Pinne
GOMAXPROCSse quiser comparar runs:GOMAXPROCS=4 go test -bench=....
Ferramentas da comunidade como benchstat ajudam a comparar dois dumps (old.txt vs new.txt) com significância estatística. O fluxo típico de PR:
go test -bench=BenchmarkEncode -benchmem -count=10 ./... > old.txt
# ... muda o código ...
go test -bench=BenchmarkEncode -benchmem -count=10 ./... > new.txt
benchstat old.txt new.txt
Bytes processados: b.SetBytes
Para parsers, compressores e codecs, reporte throughput:
func BenchmarkParse(b *testing.B) {
raw := []byte(`{"id":"x","total":1}`)
b.SetBytes(int64(len(raw)))
b.ReportAllocs()
for b.Loop() {
if _, err := ParsePedido(raw); err != nil {
b.Fatal(err)
}
}
}
A coluna extra MB/s aparece no relatório e facilita comparar implementações que processam entradas de tamanhos diferentes.
Do benchmark ao pprof
Quando o número surpreende — “por que 20 allocs?” — perfil a partir do próprio benchmark:
go test -bench=BenchmarkMarshalPedido -benchmem \
-cpuprofile=cpu.pprof \
-memprofile=mem.pprof \
./internal/jsonutil
go tool pprof -http=:0 cpu.pprof
go tool pprof -http=:0 mem.pprof
Leitura combinada:
- Benchmark diz quanto.
pprofde CPU diz quais funções.pprofde memória diz quem aloca.- Se ainda faltar a dimensão temporal (scheduler, GC, syscalls), use go tool trace.
Esse pipeline é o mesmo que times de plataforma usam em incidentes: métricas apontam o sintoma, benchmark isola o hot path, pprof explica o custo. Para instrumentação contínua em produção, o par natural é OpenTelemetry + Prometheus no ecossistema Go.
Parallel benchmarks: b.RunParallel
Para medir contenda (mutex, pool, cache), use:
func BenchmarkCacheGet(b *testing.B) {
c := NewCache()
c.Set("k", "v")
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = c.Get("k")
}
})
}
Cuidados:
b.RunParalleldistribui iterações entre goroutines; o total ainda é controlado pelo runner.- Contenda artificial demais (todos no mesmo mutex sem carga realista) produz números pessimistas.
- Contenda de menos (cada goroutine com chave exclusiva) produz números otimistas.
- Modele o padrão de acesso do seu serviço: 1 chave quente? muitas chaves? leitura 95% / escrita 5%?
Se o objetivo é reutilizar buffers entre requisições, meça com e sem sync.Pool no mesmo sub-benchmark. Pool sem prova de allocs/op é otimização cosmética.
Armadilhas que distorcem resultado
1. Medir o GC alheio
Um benchmark curto demais pode cair em fase de GC de outro teste. Use -count e, se necessário, isole o pacote. Evite rodar a suíte inteira de integração no mesmo processo se o objetivo é microbenchmark estável.
2. Comparar máquinas diferentes como se fossem iguais
ns/op no seu Mac M-series não é o mesmo no runner Linux da CI nem no pod c5.large. Compare relativo (A vs B no mesmo host), não absoluto entre ambientes.
3. Otimizar o benchmark, não o produto
Há quem reescreva o teste até o número ficar bonito: entrada minúscula, sem erro path, sem validação, sem contexto cancelável. O usuário real manda payload sujo, header estranho e timeout. Meça o caminho que a API realmente executa — inclusive validação e middleware.
4. Esquecer alocações
Uma versão 10% mais rápida com 30× mais allocs/op pode ser pior sob carga. Em serviços com worker pool e alta concorrência, GC vira latência de cauda. Olhe B/op e allocs/op sempre.
5. Misturar correção e performance
Benchmark não substitui teste. Se a versão “rápida” erra em 1% dos casos, você não ganhou performance — ganhou incidente. Mantenha testes de tabela + fuzz no mesmo pacote.
6. Timer em volta de log e I/O acidental
fmt.Printf de debug dentro do loop, gravação de arquivo, abertura de conexão: tudo isso vira o custo dominante. Deixe o corpo do loop enxuto.
Exemplo prático: JSON vs buffer reutilizável
Cenário comum em API REST Go: serializar a mesma struct em todo response. Compare alocação ingênua com encoder reutilizável:
func BenchmarkRespostaJSON(b *testing.B) {
type Resp struct {
OK bool `json:"ok"`
Msg string `json:"msg"`
Code int `json:"code"`
}
r := Resp{OK: true, Msg: "criado", Code: 201}
b.Run("marshal-each-time", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
out, err := json.Marshal(r)
if err != nil {
b.Fatal(err)
}
sink = out
}
})
b.Run("encoder-buffer", func(b *testing.B) {
b.ReportAllocs()
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
for b.Loop() {
buf.Reset()
if err := enc.Encode(r); err != nil {
b.Fatal(err)
}
// Encode adiciona '\n'; copie se precisar reter bytes
out := make([]byte, buf.Len())
copy(out, buf.Bytes())
sink = out
}
})
}
Não assuma vencedor: meça no seu tipo e tamanho de payload. Em alguns casos, json.Marshal + pool de buffers ganha; em outros, codegen ou encoding/json/v2 experimental muda o quadro — o guia de JSON em Go aprofunda o tema de APIs e streaming.
Onde encaixar benchmarks no dia a dia
- Hot path identificado por métricas (p95 da rota
/checkoutsubiu). - Escreva/ajuste teste de correção do trecho.
- Adicione
Benchmark...no mesmo pacote. - Rode com
-benchmem -count=10. - Se não souber por quê, pprof.
- Só então mude o código (pool, prealloc, outra estrutura, menos cópia).
- Re-rode e guarde o
benchstatno PR se a mudança for de performance.
Para times que usam PGO, o benchmark local não substitui perfil de produção, mas valida que a mudança de código não regrediu o caminho quente antes do deploy. Veja PGO em Go.
Benchmarks e vagas Go no Brasil
Descrições de backend Go em empresas brasileiras pedem com frequência: APIs, concorrência, PostgreSQL, Docker/Kubernetes, observabilidade e “preocupação com performance”. Saber explicar como você mediu — não só que “deixou mais rápido” — diferencia em entrevista técnica.
Um portfólio sólido mostra:
- teste de tabela + fuzz no parser;
- benchmark com
-benchmemno hot path; - pprof anexado ou documentado no README;
- health checks e graceful shutdown;
- logs com slog.
Se está buscando oportunidade, cruze com as vagas Go e com ideias de projetos para portfólio. Para comparar como outras stacks falam de performance em entrevistas e vagas, o portal eu.dev.br agrega aberturas de várias linguagens.
Perguntas frequentes
Como rodar só benchmarks, sem testes unitários?
go test -run=^$ -bench=. -benchmem ./...
O -run=^$ não casa com nenhum teste; só os benchmarks executam.
Preciso de framework de benchmark?
Não. O pacote testing cobre a maior parte dos casos. Frameworks externos raramente valem a pena só para microbenchmark de biblioteca. Invista em cenários realistas e em perfis.
Benchmark em CI deve falhar o build?
Depende da maturidade. Comece coletando e publicando o artefato (benchstat em comentário de PR). Falhar o build por 2% de ruído em runner compartilhado gera alerta-fadiga. Falhar por regressão grande e estável (por exemplo +50% ns/op ou +10 allocs/op no hot path) pode fazer sentido com limiares folgados.
Posso benchmarkar código com time.Now ou rand?
Sim, mas controle a fonte. Injete relógio e gerador (interfaces pequenas — o mesmo espírito de DI sem framework) para o benchmark ser determinístico. Caso contrário, parte da variância é do próprio pseudoaleatório.
Qual a relação com fuzzing?
Fuzz encontra entradas que quebram correção; benchmark mede custo de entradas representativas. Use os dois: o corpus do fuzz às vezes vira ótimo input de benchmark para parsers.
Checklist final
- Nome do benchmark descreve a operação real.
- Correção coberta por teste antes da medição.
- Preferir
for b.Loop()em código novo. - Setup caro fora do loop +
ResetTimerse necessário. - Resultado observado (anti dead-code elimination).
-
-benchmemouReportAllocssempre no hot path. -
-count≥5(ideal 10) antes de cravar conclusão. - Comparar A vs B no mesmo ambiente.
- Surpresa → pprof (CPU/mem) → trace se for temporal.
- Não otimizar alloc zero no teste se a produção aloca por validação/log/middleware.
Benchmark bom é chato e honesto: entrada realista, ambiente estável, números com alocação, e mudança de código só depois da prova. Em Go, a ferramenta já vem na standard library — o diferencial é disciplina para não se enganar com um ns/op bonito.
Próximos passos naturais: instrumentar o serviço com OpenTelemetry, expor métricas Prometheus, e quando o p95 apitar, voltar a este fluxo com um Benchmark no pacote culpado. Se quiser praticar com um serviço completo, siga o tutorial de API REST em Go e adicione benchmarks nos handlers de serialização e nos clients HTTP críticos.