---
title: "Benchmarks em Go: testing.B, benchmem e pprof"
url: "https://golang.com.br/blog/benchmarks-go-testing-b-benchmem-pprof/"
markdown_url: "https://golang.com.br/blog/benchmarks-go-testing-b-benchmem-pprof.MD"
description: "Aprenda benchmarks em Go com testing.B, b.Loop, go test -bench, -benchmem, sub-benchmarks, alocações, pprof e armadilhas que distorcem resultados em produção."
date: "2026-07-28"
author: "Golang Brasil"
---

# Benchmarks em Go: testing.B, benchmem e pprof

Aprenda benchmarks em Go com testing.B, b.Loop, go test -bench, -benchmem, sub-benchmarks, alocações, pprof e armadilhas que distorcem resultados em produção.


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](/blog/pprof-go-producao/).

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](/blog/testes-tabela-go-guia-table-driven-tests/), [fuzzing nativo](/blog/fuzzing-go-testes-nativos/), [sync.Pool](/blog/sync-pool-go-reutilizacao-objetos-performance/), [PGO](/blog/go-pgo-profile-guided-optimization-performance/), [go tool trace](/blog/go-tool-trace-runtime-trace-latencia-goroutines/) e o [guia de testes](/aprenda/testes-go/).

## 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:

```go
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:

```bash
go test -bench=BenchmarkMarshalPedido -benchmem ./internal/jsonutil
```

Saída típica:

```text
BenchmarkMarshalPedido-8    2500000    480.2 ns/op    128 B/op    2 allocs/op
```

Leitura:

- **`-8`**: `GOMAXPROCS` no 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](/blog/green-tea-garbage-collector/) 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:

```go
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`:

```go
func BenchmarkConcatLoop(b *testing.B) {
	for b.Loop() {
		_ = "go" + "lang"
	}
}
```

Com `b.Loop()`:

1. O runner controla quantas vezes o corpo executa.
2. O timing fica mais robusto em relação a setup e finalização.
3. Várias classes de dead-code elimination e de contagem errada de `b.N` deixam 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`:

```go
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 `-benchmem` naquele 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](/blog/go-testcontainers-testes-integracao-containers/), 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:

```go
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:

```bash
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](/blog/testes-tabela-go-guia-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:

```go
// 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:

```go
var globalSink any

func BenchmarkEncodeBom(b *testing.B) {
	var local any
	for b.Loop() {
		local = Encode(pedido)
	}
	globalSink = local
}
```

Ou, quando há erro:

```go
for b.Loop() {
	out, err := Encode(pedido)
	if err != nil {
		b.Fatal(err)
	}
	globalSink = out
}
```

Checklist anti-mentira:

1. Use o retorno (atribuir a `sink` global ou verificar erro).
2. Não meça só o caminho que aloca se a versão "rápida" só parece rápida porque foi otimizada para zero.
3. 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:

```bash
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:

```bash
go test -bench=BenchmarkMarshalPedido -benchtime=3s -benchmem ./internal/jsonutil
```

Ou por iterações fixas:

```bash
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 `GOMAXPROCS` se 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:

```bash
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:

```go
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:

```bash
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:

1. Benchmark diz **quanto**.
2. `pprof` de CPU diz **quais funções**.
3. `pprof` de memória diz **quem aloca**.
4. Se ainda faltar a dimensão temporal (scheduler, GC, syscalls), use [go tool trace](/blog/go-tool-trace-runtime-trace-latencia-goroutines/).

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](/blog/go-opentelemetry-observabilidade-tracing-metricas/) + [Prometheus no ecossistema Go](/tutoriais/go-prometheus/).

## Parallel benchmarks: b.RunParallel

Para medir contenda (mutex, pool, cache), use:

```go
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.RunParallel` distribui 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](/blog/sync-pool-go-reutilizacao-objetos-performance/) 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](/blog/middleware-go-net-http-chi-gin-producao/).

### 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](/blog/worker-pool-go-fila-jobs/) 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:

```go
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](/blog/json-go-encoding-json-v2-streaming-validacao/) aprofunda o tema de APIs e streaming.

## Onde encaixar benchmarks no dia a dia

1. **Hot path identificado por métricas** (p95 da rota `/checkout` subiu).
2. **Escreva/ajuste teste de correção** do trecho.
3. **Adicione `Benchmark...` no mesmo pacote**.
4. **Rode com `-benchmem -count=10`**.
5. **Se não souber por quê, pprof**.
6. **Só então mude o código** (pool, prealloc, outra estrutura, menos cópia).
7. **Re-rode e guarde o `benchstat` no 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](/blog/go-pgo-profile-guided-optimization-performance/).

## 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](/carreira/entrevista-tecnica-go-2026/).

Um portfólio sólido mostra:

- teste de tabela + fuzz no parser;
- benchmark com `-benchmem` no hot path;
- pprof anexado ou documentado no README;
- [health checks](/blog/health-checks-go-liveness-readiness-startup/) e [graceful shutdown](/blog/graceful-shutdown-go-producao/);
- logs com [slog](/blog/slog-go-logging-estruturado/).

Se está buscando oportunidade, cruze com as [vagas Go](/vagas/) e com ideias de [projetos para portfólio](/blog/projetos-go-para-portfolio-ideias-primeira-vaga/). Para comparar como outras stacks falam de performance em entrevistas e vagas, o portal <a href="https://eu.dev.br/vagas/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'eu.dev.br' })">eu.dev.br</a> agrega aberturas de várias linguagens.

## Perguntas frequentes

### Como rodar só benchmarks, sem testes unitários?

```bash
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](/blog/dependency-injection-go-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 + `ResetTimer` se necessário.
- [ ] Resultado observado (anti dead-code elimination).
- [ ] `-benchmem` ou `ReportAllocs` sempre 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](/blog/go-opentelemetry-observabilidade-tracing-metricas/), expor [métricas Prometheus](/tutoriais/go-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](/aprenda/api-rest-go/) e adicione benchmarks nos handlers de serialização e nos clients HTTP críticos.
