<h2>Introdução aos Benchmarks em Go</h2>
<p>Quando desenvolvemos software em Go, é comum nos deparar com a necessidade de otimizar nosso código. Mas como saber se nossas otimizações realmente funcionam? A resposta está em medir. Go oferece ferramentas nativas e poderosas para isso, e começamos com o pacote <code>testing</code>, que não apenas executa testes, mas também nos permite criar benchmarks robustos e confiáveis.</p>
<p>Um benchmark é essencialmente um teste que executa uma função repetidamente e mede quanto tempo leva, quantas alocações de memória ocorrem e outras métricas. Diferentemente de um teste unitário que verifica se algo está correto, um benchmark responde à pergunta: "quão rápido isso é?". Isso é fundamental quando você está otimizando um algoritmo crítico ou decidindo entre duas implementações.</p>
<h2>Testing.B: Escrevendo Benchmarks Profissionais</h2>
<h3>Estrutura Básica de um Benchmark</h3>
<p>Um benchmark em Go é uma função que segue o padrão <code>BenchmarkNomeDoTeste(b <em>testing.B)</code>. A interface <code></em>testing.B</code> nos fornece um loop que executa nosso código várias vezes enquanto coleta informações sobre tempo e memória. O mais importante é usar <code>b.N</code> — uma variável que Go ajusta automaticamente para garantir que o benchmark rode tempo suficiente (geralmente alguns segundos).</p>
<p>Vamos começar com um exemplo prático. Imagine que temos duas funções para buscar o maior número em um slice:</p>
<pre><code class="language-go">package main
import (
"testing"
)
// Implementação 1: iteração simples
func FindMaxSimple(nums []int) int {
if len(nums) == 0 {
return 0
}
max := nums[0]
for _, num := range nums {
if num > max {
max = num
}
}
return max
}
// Implementação 2: com early return (não é melhor neste caso, mas ilustra)
func FindMaxOptimized(nums []int) int {
if len(nums) == 0 {
return 0
}
max := nums[0]
for i := 1; i < len(nums); i++ {
if nums[i] > max {
max = nums[i]
}
}
return max
}
// Benchmark da implementação simples
func BenchmarkFindMaxSimple(b *testing.B) {
nums := make([]int, 10000)
for i := 0; i < len(nums); i++ {
nums[i] = i
}
b.ResetTimer() // Reseta o timer, ignorando o tempo de setup
for i := 0; i < b.N; i++ {
FindMaxSimple(nums)
}
}
// Benchmark da implementação otimizada
func BenchmarkFindMaxOptimized(b *testing.B) {
nums := make([]int, 10000)
for i := 0; i < len(nums); i++ {
nums[i] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
FindMaxOptimized(nums)
}
}</code></pre>
<p>Para executar esses benchmarks, você usa:</p>
<pre><code class="language-bash">go test -bench=. -benchmem</code></pre>
<p>A flag <code>-bench=.</code> executa todos os benchmarks, e <code>-benchmem</code> mostra alocações de memória. A saída será algo como:</p>
<pre><code>BenchmarkFindMaxSimple-8 500000 2150 ns/op 0 B/op 0 allocs/op
BenchmarkFindMaxOptimized-8 500000 2140 ns/op 0 B/op 0 allocs/op</code></pre>
<p>Isso significa que cada operação leva aproximadamente 2150 nanossegundos, e nenhuma alocação de memória ocorre.</p>
<h3>Técnicas Avançadas com testing.B</h3>
<p>Às vezes você quer fazer benchmarks mais sofisticados. A interface <code>testing.B</code> oferece vários métodos úteis além de <code>ResetTimer()</code>. Vamos explorar um cenário real: benchmarking de diferentes estratégias de parsing de JSON.</p>
<pre><code class="language-go">package main
import (
"encoding/json"
"testing"
)
type User struct {
ID int json:"id"
Name string json:"name"
Email string json:"email"
}
var jsonData = {"id":1,"name":"João Silva","email":"joao@example.com"}
// Benchmark básico com alocação no loop
func BenchmarkUnmarshalBasic(b *testing.B) {
for i := 0; i < b.N; i++ {
var user User
json.Unmarshal([]byte(jsonData), &user)
}
}
// Benchmark reutilizando o slice de bytes
func BenchmarkUnmarshalOptimized(b *testing.B) {
data := []byte(jsonData)
b.ResetTimer()
for i := 0; i < b.N; i++ {
var user User
json.Unmarshal(data, &user)
}
}
// Sub-benchmarks para diferentes tamanhos de JSON
func BenchmarkUnmarshalVariousSizes(b *testing.B) {
tests := []struct {
name string
json string
}{
{"Small", {"id":1,"name":"A","email":"a@a.com"}},
{"Medium", {"id":1,"name":"João da Silva","email":"joao.silva@company.com"}},
{"Large", {"id":1,"name":"Maria Clara da Silva Santos","email":"maria.clara.silva.santos@very-long-company-name.com.br"}},
}
for _, test := range tests {
b.Run(test.name, func(b *testing.B) {
data := []byte(test.json)
for i := 0; i < b.N; i++ {
var user User
json.Unmarshal(data, &user)
}
})
}
}</code></pre>
<p>Quando você executa <code>go test -bench=BenchmarkUnmarshalVariousSizes -benchmem</code>, verá:</p>
<pre><code>BenchmarkUnmarshalVariousSizes/Small-8 500000 2410 ns/op 288 B/op 3 allocs/op
BenchmarkUnmarshalVariousSizes/Medium-8 500000 2680 ns/op 336 B/op 3 allocs/op
BenchmarkUnmarshalVariousSizes/Large-8 500000 3050 ns/op 432 B/op 3 allocs/op</code></pre>
<p>Este padrão é excelente porque permite comparar o desempenho sob diferentes condições sem duplicar código. Note também que <code>b.ResetTimer()</code> é essencial quando você faz setup que não quer medir — como criar slices ou dados de entrada.</p>
<h2>Profiling com pprof: Descobrindo Gargalos Reais</h2>
<h3>CPU Profiling: Encontrando Onde o Tempo Vai</h3>
<p>Benchmarks nos dão números, mas pprof nos mostra exatamente onde nosso programa está gastando tempo. O profiler de CPU é a ferramenta mais comum para começar. Vamos criar um exemplo que simula um problema real: processamento ineficiente de strings.</p>
<pre><code class="language-go">package main
import (
"strings"
)
// Função ineficiente: cria muitas strings intermediárias
func ProcessStringsInefficient(texts []string) []string {
result := make([]string, len(texts))
for i, text := range texts {
// Cada operação de string cria uma nova alocação
processed := text
processed = strings.ToUpper(processed)
processed = strings.TrimSpace(processed)
processed = strings.ReplaceAll(processed, "A", "X")
result[i] = processed
}
return result
}
// Função eficiente: operações em pipeline
func ProcessStringsEfficient(texts []string) []string {
result := make([]string, len(texts))
for i, text := range texts {
// Reutiliza a string através de operações encadeadas
result[i] = strings.ReplaceAll(
strings.TrimSpace(strings.ToUpper(text)),
"A",
"X",
)
}
return result
}</code></pre>
<p>Para fazer CPU profiling, você executa:</p>
<pre><code class="language-bash">go test -bench=BenchmarkProcess -cpuprofile=cpu.prof</code></pre>
<p>Isso gera um arquivo <code>cpu.prof</code>. Então você analisa com:</p>
<pre><code class="language-bash">go tool pprof cpu.prof</code></pre>
<p>Isso abre um prompt interativo. Digite <code>top</code> para ver as funções que usam mais CPU:</p>
<pre><code>(pprof) top
Showing nodes accounting for 1250ms, 95.5% of 1310ms total
flat flat% sum% cum cum%
800ms 61.1% 61.1% 850ms 64.9% strings.(*Builder).grow
250ms 19.1% 80.2% 250ms 19.1% runtime.memclr
150ms 11.5% 91.7% 950ms 72.5% strings.(*Builder).WriteRune
50ms 3.8% 95.5% 1250ms 95.5% main.ProcessStringsInefficient</code></pre>
<p>Você também pode gerar um gráfico (se tiver Graphviz instalado):</p>
<pre><code class="language-bash">go tool pprof -http=:8080 cpu.prof</code></pre>
<p>Isso abre uma interface web mostrando as relações de chamadas e quanto tempo cada uma leva.</p>
<h3>Memory Profiling: Encontrando Vazamentos e Alocações Desnecessárias</h3>
<p>Às vezes o problema não é CPU, mas memória. Go oferece memory profiling que detecta alocações excessivas. Vamos criar um benchmark que gera bastante lixo:</p>
<pre><code class="language-go">package main
import (
"testing"
)
// Aloca muitas strings temporárias
func BuildStringInefficient(n int) string {
result := ""
for i := 0; i < n; i++ {
result = result + "item" // Cria uma nova string a cada iteração
}
return result
}
// Usa strings.Builder, muito mais eficiente
func BuildStringEfficient(n int) string {
var builder strings.Builder
for i := 0; i < n; i++ {
builder.WriteString("item")
}
return builder.String()
}
func BenchmarkBuildStringInefficient(b *testing.B) {
for i := 0; i < b.N; i++ {
BuildStringInefficient(1000)
}
}
func BenchmarkBuildStringEfficient(b *testing.B) {
for i := 0; i < b.N; i++ {
BuildStringEfficient(1000)
}
}</code></pre>
<p>Execute com:</p>
<pre><code class="language-bash">go test -bench=BuildString -benchmem -memprofile=mem.prof</code></pre>
<p>Analise com:</p>
<pre><code class="language-bash">go tool pprof mem.prof
go tool pprof -http=:8080 mem.prof</code></pre>
<p>A saída mostrará algo como:</p>
<pre><code>Showing nodes accounting for 25.5MB, 98.1% of 26MB total
flat flat% sum% cum cum%
24MB 92.3% 92.3% 24MB 92.3% main.BuildStringInefficient
1.5MB 5.8% 98.1% 1.5MB 5.8% main.BuildStringEfficient</code></pre>
<p>A função ineficiente está alocando 24MB enquanto a eficiente usa apenas 1.5MB — uma diferença colossal! Isso ilustra por que profiling é essencial: algumas otimizações têm impacto real.</p>
<h3>Análise Combinada: CPU + Memória</h3>
<p>Em cenários do mundo real, você quer ambas as informações. Aqui está como executar e analisar um benchmark completo:</p>
<pre><code class="language-bash"># Executa benchmarks com profiling de CPU e memória
go test -bench=. -benchtime=5s -cpuprofile=cpu.prof -memprofile=mem.prof
Analisa CPU
go tool pprof cpu.prof
Dentro do pprof, você pode:
list <funcname> - mostra o código com tempo por linha
web - gera um gráfico (precisa de graphviz)
top - mostra as top funções
Para comparar dois profiles (útil para antes/depois de otimização):
go test -bench=. -benchmem > before.txt
... faz suas otimizações ...
go test -bench=. -benchmem > after.txt
benchstat before.txt after.txt</code></pre>
<p>O <code>benchstat</code> é particularmente poderoso:</p>
<pre><code class="language-bash">go install golang.org/x/perf/cmd/benchstat@latest
benchstat before.txt after.txt</code></pre>
<p>Saída esperada:</p>
<pre><code>name old time/op new time/op delta
BuildStringInefficient-8 5.42ms ± 2% 0.18ms ± 1% -96.68% (p=0.000 n=10)
BuildStringEfficient-8 0.17ms ± 1% 0.17ms ± 1% -0.29% (p=0.562 n=10)
name old alloc/op new alloc/op delta
BuildStringInefficient-8 18.2MB ± 0% 18.2MB ± 0% 0.00% (p=1.000 n=10)
BuildStringEfficient-8 1.51MB ± 0% 1.51MB ± 0% 0.00% (p=1.000 n=10)</code></pre>
<p>Isso quantifica exatamente a melhoria — 96.68% mais rápido neste caso!</p>
<h2>Integração com CI/CD e Boas Práticas</h2>
<h3>Automatizando Benchmarks na Pipeline</h3>
<p>Em um projeto profissional, você quer acompanhar benchmarks ao longo do tempo. Uma abordagem comum é manter histórico de resultados:</p>
<pre><code class="language-bash">#!/bin/bash
benchmark.sh
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_DIR="benchmark_results"
mkdir -p "$OUTPUT_DIR"
Executa benchmarks e salva resultado
go test -bench=. -benchmem -run=^$ > "$OUTPUT_DIR/result_${TIMESTAMP}.txt"
Se você tem um resultado anterior, compara
if [ -f "$OUTPUT_DIR/baseline.txt" ]; then
benchstat "$OUTPUT_DIR/baseline.txt" "$OUTPUT_DIR/result_${TIMESTAMP}.txt"
fi
Atualiza baseline
cp "$OUTPUT_DIR/result_${TIMESTAMP}.txt" "$OUTPUT_DIR/baseline.txt"</code></pre>
<p>Adicione isso ao seu <code>Makefile</code>:</p>
<pre><code class="language-makefile">.PHONY: bench
bench:
go test -bench=. -benchmem -benchtime=3s
.PHONY: profile
profile:
go test -bench=. -cpuprofile=cpu.prof -memprofile=mem.prof
go tool pprof -http=:8080 cpu.prof
.PHONY: bench-compare
bench-compare:
@if [ ! -f /tmp/bench_before.txt ]; then \
echo "Running baseline benchmark..."; \
go test -bench=. -benchmem > /tmp/bench_before.txt; \
fi
@go test -bench=. -benchmem > /tmp/bench_after.txt
@echo "Comparison results:"
@benchstat /tmp/bench_before.txt /tmp/bench_after.txt</code></pre>
<h3>Padrões de Benchmark Realistas</h3>
<p>Um erro comum é fazer benchmarks que não refletem uso real. Aqui está um exemplo de benchmark bem feito para uma cache:</p>
<pre><code class="language-go">package cache
import (
"testing"
)
type Cache struct {
data map[string]interface{}
}
func NewCache() *Cache {
return &Cache{data: make(map[string]interface{})}
}
func (c *Cache) Set(key string, value interface{}) {
c.data[key] = value
}
func (c *Cache) Get(key string) (interface{}, bool) {
val, ok := c.data[key]
return val, ok
}
// Benchmark realista: mistura de reads e writes (80/20)
func BenchmarkCacheRealistic(b *testing.B) {
cache := NewCache()
// Pré-popula com dados
for i := 0; i < 1000; i++ {
cache.Set("key"+string(rune(i)), i)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if i%5 == 0 {
// 20% writes
cache.Set("key_new", i)
} else {
// 80% reads
cache.Get("key500")
}
}
}
// Benchmark paralelo: simula concorrência real
func BenchmarkCacheParallel(b *testing.B) {
cache := NewCache()
for i := 0; i < 1000; i++ {
cache.Set("key"+string(rune(i)), i)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
if i%5 == 0 {
cache.Set("key_new", i)
} else {
cache.Get("key500")
}
i++
}
})
}</code></pre>
<p>Execute com:</p>
<pre><code class="language-bash">go test -bench=Cache -benchmem -benchtime=5s</code></pre>
<p>O <code>RunParallel</code> é crucial — ele simula múltiplas goroutines usando a mesma função, o que é muito mais realista que um benchmark sequencial.</p>
<h2>Conclusão</h2>
<p>Dominar benchmarking e profiling em Go é essencial para escrever software robusto e performático. Primeiro, use <code>testing.B</code> para medir quantitativamente o desempenho do seu código — é simples, nativo e oferece resultados confiáveis. Segundo, quando números não bastam, use pprof para descobrir exatamente onde o tempo e a memória estão sendo gastos, revelando gargalos que não são óbvios. Terceiro, integre essas práticas na sua pipeline de CI/CD e sempre compare antes/depois de otimizações usando <code>benchstat</code> — isso transforma profiling de uma atividade ocasional em uma disciplina contínua.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://pkg.go.dev/testing" target="_blank" rel="noopener noreferrer">Documentação oficial: testing package</a></li>
<li><a href="https://blog.golang.org/profiling-go-programs" target="_blank" rel="noopener noreferrer">Go blog: Profiling Go Programs</a></li>
<li><a href="https://github.com/google/pprof/tree/master/doc" target="_blank" rel="noopener noreferrer">Go PProf documentation</a></li>
<li><a href="https://go.dev/doc/effective_go#Concurrency" target="_blank" rel="noopener noreferrer">High Performance Go</a></li>
<li><a href="https://pkg.go.dev/golang.org/x/perf/cmd/benchstat" target="_blank" rel="noopener noreferrer">Benchstat tool - Go performance benchmarking</a></li>
</ul>
<p><!-- FIM --></p>