Go

Como Usar Benchmarks e Profiling em Go: testing.B e pprof na Prática em Produção

15 min de leitura

Como Usar Benchmarks e Profiling em Go: testing.B e pprof na Prática em Produção

Introdução aos Benchmarks em Go 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 , que não apenas executa testes, mas também nos permite criar benchmarks robustos e confiáveis. 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. Testing.B: Escrevendo Benchmarks Profissionais Estrutura Básica de um Benchmark Um benchmark em Go é uma função que segue o padrão . A interface nos fornece um loop que executa nosso código várias vezes enquanto coleta informações sobre tempo e

<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: &quot;quão rápido isso é?&quot;. 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 (

&quot;testing&quot;

)

// 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 &gt; 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 &lt; len(nums); i++ {

if nums[i] &gt; max {

max = nums[i]

}

}

return max

}

// Benchmark da implementação simples

func BenchmarkFindMaxSimple(b *testing.B) {

nums := make([]int, 10000)

for i := 0; i &lt; len(nums); i++ {

nums[i] = i

}

b.ResetTimer() // Reseta o timer, ignorando o tempo de setup

for i := 0; i &lt; b.N; i++ {

FindMaxSimple(nums)

}

}

// Benchmark da implementação otimizada

func BenchmarkFindMaxOptimized(b *testing.B) {

nums := make([]int, 10000)

for i := 0; i &lt; len(nums); i++ {

nums[i] = i

}

b.ResetTimer()

for i := 0; i &lt; 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 (

&quot;encoding/json&quot;

&quot;testing&quot;

)

type User struct {

ID int json:&quot;id&quot;

Name string json:&quot;name&quot;

Email string json:&quot;email&quot;

}

var jsonData = {&quot;id&quot;:1,&quot;name&quot;:&quot;João Silva&quot;,&quot;email&quot;:&quot;joao@example.com&quot;}

// Benchmark básico com alocação no loop

func BenchmarkUnmarshalBasic(b *testing.B) {

for i := 0; i &lt; b.N; i++ {

var user User

json.Unmarshal([]byte(jsonData), &amp;user)

}

}

// Benchmark reutilizando o slice de bytes

func BenchmarkUnmarshalOptimized(b *testing.B) {

data := []byte(jsonData)

b.ResetTimer()

for i := 0; i &lt; b.N; i++ {

var user User

json.Unmarshal(data, &amp;user)

}

}

// Sub-benchmarks para diferentes tamanhos de JSON

func BenchmarkUnmarshalVariousSizes(b *testing.B) {

tests := []struct {

name string

json string

}{

{&quot;Small&quot;, {&quot;id&quot;:1,&quot;name&quot;:&quot;A&quot;,&quot;email&quot;:&quot;a@a.com&quot;}},

{&quot;Medium&quot;, {&quot;id&quot;:1,&quot;name&quot;:&quot;João da Silva&quot;,&quot;email&quot;:&quot;joao.silva@company.com&quot;}},

{&quot;Large&quot;, {&quot;id&quot;:1,&quot;name&quot;:&quot;Maria Clara da Silva Santos&quot;,&quot;email&quot;:&quot;maria.clara.silva.santos@very-long-company-name.com.br&quot;}},

}

for _, test := range tests {

b.Run(test.name, func(b *testing.B) {

data := []byte(test.json)

for i := 0; i &lt; b.N; i++ {

var user User

json.Unmarshal(data, &amp;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 (

&quot;strings&quot;

)

// 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, &quot;A&quot;, &quot;X&quot;)

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)),

&quot;A&quot;,

&quot;X&quot;,

)

}

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 (

&quot;testing&quot;

)

// Aloca muitas strings temporárias

func BuildStringInefficient(n int) string {

result := &quot;&quot;

for i := 0; i &lt; n; i++ {

result = result + &quot;item&quot; // 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 &lt; n; i++ {

builder.WriteString(&quot;item&quot;)

}

return builder.String()

}

func BenchmarkBuildStringInefficient(b *testing.B) {

for i := 0; i &lt; b.N; i++ {

BuildStringInefficient(1000)

}

}

func BenchmarkBuildStringEfficient(b *testing.B) {

for i := 0; i &lt; 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 &lt;funcname&gt; - 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 &gt; before.txt

... faz suas otimizações ...

go test -bench=. -benchmem &gt; 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=&quot;benchmark_results&quot;

mkdir -p &quot;$OUTPUT_DIR&quot;

Executa benchmarks e salva resultado

go test -bench=. -benchmem -run=^$ &gt; &quot;$OUTPUT_DIR/result_${TIMESTAMP}.txt&quot;

Se você tem um resultado anterior, compara

if [ -f &quot;$OUTPUT_DIR/baseline.txt&quot; ]; then

benchstat &quot;$OUTPUT_DIR/baseline.txt&quot; &quot;$OUTPUT_DIR/result_${TIMESTAMP}.txt&quot;

fi

Atualiza baseline

cp &quot;$OUTPUT_DIR/result_${TIMESTAMP}.txt&quot; &quot;$OUTPUT_DIR/baseline.txt&quot;</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 &quot;Running baseline benchmark...&quot;; \

go test -bench=. -benchmem &gt; /tmp/bench_before.txt; \

fi

@go test -bench=. -benchmem &gt; /tmp/bench_after.txt

@echo &quot;Comparison results:&quot;

@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 (

&quot;testing&quot;

)

type Cache struct {

data map[string]interface{}

}

func NewCache() *Cache {

return &amp;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 &lt; 1000; i++ {

cache.Set(&quot;key&quot;+string(rune(i)), i)

}

b.ResetTimer()

for i := 0; i &lt; b.N; i++ {

if i%5 == 0 {

// 20% writes

cache.Set(&quot;key_new&quot;, i)

} else {

// 80% reads

cache.Get(&quot;key500&quot;)

}

}

}

// Benchmark paralelo: simula concorrência real

func BenchmarkCacheParallel(b *testing.B) {

cache := NewCache()

for i := 0; i &lt; 1000; i++ {

cache.Set(&quot;key&quot;+string(rune(i)), i)

}

b.ResetTimer()

b.RunParallel(func(pb *testing.PB) {

i := 0

for pb.Next() {

if i%5 == 0 {

cache.Set(&quot;key_new&quot;, i)

} else {

cache.Get(&quot;key500&quot;)

}

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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

Boas Práticas de Ponteiros em Go: Endereços, Dereferência e Passagem por Referência para Times Ágeis
Boas Práticas de Ponteiros em Go: Endereços, Dereferência e Passagem por Referência para Times Ágeis

O que é um Ponteiro em Go Um ponteiro é uma variável que armazena o endereço...

Guia Completo de Chi Router em Go: Roteamento Idiomático e Middlewares Componíveis
Guia Completo de Chi Router em Go: Roteamento Idiomático e Middlewares Componíveis

O que é Chi e Por que Ele Importa Chi é um roteador HTTP leve e expressivo pa...

Migrations em Go com golang-migrate: Versionando o Banco de Dados: Do Básico ao Avançado
Migrations em Go com golang-migrate: Versionando o Banco de Dados: Do Básico ao Avançado

O que são Migrations e Por que Você Precisa Delas Migrations são scripts vers...