Artigo 43 — Profiling e otimização de performance em Go
Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 7 — Ferramentas, CLI e Qualidade
Otimizar sem medir é adivinhar
A regra mais importante de otimização de performance é também a mais frequentemente ignorada: nunca otimize sem antes medir. Código que parece lento pode estar em um caminho raramente executado. Código que parece eficiente pode estar alocando memória em escala invisível a olho nu.
Go oferece um ecossistema completo de profiling integrado ao toolchain — sem dependências externas, sem instrumentação manual invasiva. O pprof captura CPU, memória, goroutines, contention de locks e muito mais. O trace captura a execução em linha do tempo. Juntos, eles tornam a otimização uma atividade baseada em evidências.
Benchmarks: a base da otimização
Antes de perfilar um sistema completo, isole a função suspeita em um benchmark:
package processamento
import (
"strings"
"testing"
)
// Função candidata a otimização
func concatenarStrings(partes []string) string {
resultado := ""
for _, p := range partes {
resultado += p // alocação a cada iteração
}
return resultado
}
func concatenarComBuilder(partes []string) string {
var sb strings.Builder
sb.Grow(estimarTamanho(partes))
for _, p := range partes {
sb.WriteString(p)
}
return sb.String()
}
func estimarTamanho(partes []string) int {
total := 0
for _, p := range partes {
total += len(p)
}
return total
}
// Benchmarks
func BenchmarkConcatenarStrings(b *testing.B) {
partes := []string{"Go", " é", " uma", " linguagem", " eficiente"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
concatenarStrings(partes)
}
}
func BenchmarkConcatenarComBuilder(b *testing.B) {
partes := []string{"Go", " é", " uma", " linguagem", " eficiente"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
concatenarComBuilder(partes)
}
}
# Executar benchmarks com métricas de memória
go test -bench=. -benchmem -count=5 ./processamento/
# Saída típica
# BenchmarkConcatenarStrings-8 3000000 450 ns/op 128 B/op 4 allocs/op
# BenchmarkConcatenarComBuilder-8 10000000 120 ns/op 48 B/op 1 allocs/op
pprof: profiling de CPU
Integração em servidores HTTP
package main
import (
"net/http"
_ "net/http/pprof" // importação em branco registra os handlers automaticamente
)
func main() {
// Servidor principal
go func() {
mux := http.NewServeMux()
mux.HandleFunc("/", handlerPrincipal)
http.ListenAndServe(":8080", mux)
}()
// Servidor de profiling — nunca exponha em produção sem autenticação
http.ListenAndServe(":6060", nil) // usa DefaultServeMux com pprof registrado
}
Coletando perfis via HTTP
# Perfil de CPU por 30 segundos
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# Perfil de alocações de memória
go tool pprof http://localhost:6060/debug/pprof/heap
# Goroutines ativas
go tool pprof http://localhost:6060/debug/pprof/goroutine
# Contention de mutex
go tool pprof http://localhost:6060/debug/pprof/mutex
# Contention de channel (block profile)
go tool pprof http://localhost:6060/debug/pprof/block
Profiling em benchmarks
# Gera perfil de CPU durante benchmark
go test -bench=BenchmarkConcatenarStrings \
-cpuprofile=cpu.prof \
-memprofile=mem.prof \
-benchmem \
./processamento/
# Analisa o perfil de CPU
go tool pprof cpu.prof
# Analisa o perfil de memória
go tool pprof mem.prof
Comandos interativos do pprof
(pprof) top # top N funções por tempo de CPU
(pprof) top10 -cum # top 10 por tempo acumulado (inclui subfunções)
(pprof) list NomeFuncao # detalha linha a linha uma função específica
(pprof) web # abre visualização gráfica no navegador (requer graphviz)
(pprof) png # gera imagem PNG do grafo de chamadas
(pprof) svg # gera SVG interativo
Exemplo de sessão interativa:
$ go tool pprof cpu.prof
Type: cpu
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top10
Showing nodes accounting for 2.3s, 92.00% of 2.5s total
flat flat% sum% cum cum%
1.20s 48.00% 48.00% 1.20s 48.00% runtime.mallocgc
0.50s 20.00% 68.00% 0.50s 20.00% runtime.memmove
0.30s 12.00% 80.00% 1.80s 72.00% processamento.concatenarStrings
...
(pprof) list concatenarStrings
Total: 2.5s
ROUTINE ======================== processamento.concatenarStrings
300ms 1.80s (flat, cum) 72.00% of Total
. . 10: func concatenarStrings(partes []string) string {
. . 11: resultado := ""
50ms 1.50s 12: for _, p := range partes {
250ms 300ms 13: resultado += p
. . 14: }
. . 15: return resultado
. . 16: }
pprof: visualização web integrada
# Interface web completa — mais útil que o modo interativo
go tool pprof -http=:8090 cpu.prof
Isso abre um servidor web com: - Graph — grafo de chamadas com pesos visuais - Flame Graph — visualização de flamegraph interativa - Top — tabela ordenada por custo - Source — código anotado com custos por linha - Disasm — assembly anotado
O flamegraph é especialmente útil: o eixo X representa proporção de tempo, o eixo Y representa profundidade da pilha de chamadas. Barras largas no topo são onde o tempo é gasto.
Profiling de memória em profundidade
package main
import (
"os"
"runtime"
"runtime/pprof"
)
func perfilarMemoria(caminho string) func() {
f, _ := os.Create(caminho)
return func() {
runtime.GC() // força GC para perfil mais preciso
pprof.WriteHeapProfile(f)
f.Close()
}
}
func main() {
encerrar := perfilarMemoria("mem.prof")
defer encerrar()
// Código a perfilar
processar()
}
Tipos de perfil de memória:
# Objetos alocados que ainda estão vivos (heap inuse)
go tool pprof -alloc_objects mem.prof
# Espaço total alocado desde o início (inclui objetos já coletados)
go tool pprof -alloc_space mem.prof
# Objetos em uso no momento da captura
go tool pprof -inuse_objects mem.prof
# Espaço em uso no momento da captura
go tool pprof -inuse_space mem.prof
Trace: visualização temporal da execução
O trace captura eventos com timestamps precisos — goroutines criadas, GC pausas, chamadas de sistema, preempções:
package main
import (
"os"
"runtime/trace"
)
func main() {
f, _ := os.Create("trace.out")
defer f.Close()
trace.Start(f)
defer trace.Stop()
// Código a analisar
executarTarefas()
}
# Analisar o trace
go tool trace trace.out
O trace viewer exibe: - Goroutine Analysis — tempo em cada estado (running, runnable, blocked, sleeping) - GC pausas — duração e frequência - Scheduler latency — tempo esperando para ser escalonado - Syscall duration — tempo em chamadas de sistema
Trace em benchmarks
go test -bench=BenchmarkProcessar -trace=trace.out ./...
go tool trace trace.out
Identificando e corrigindo problemas comuns
Alocações excessivas: escape analysis
// Descobre o que escapa para o heap
go build -gcflags="-m" ./...
go build -gcflags="-m -m" ./... # mais detalhado
// PROBLEMA: struct escapa para o heap desnecessariamente
func criarPonto() *Ponto {
return &Ponto{X: 1, Y: 2} // escapa para heap
}
// MELHOR: para funções frequentemente chamadas, prefira retorno por valor
func criarPontoValor() Ponto {
return Ponto{X: 1, Y: 2} // fica na stack
}
// PROBLEMA: interface causa boxing e escape para heap
func processar(v interface{}) {
_ = v
}
// MELHOR: tipo concreto quando possível
func processarInt(v int) {
_ = v
}
Slices: pré-alocação
// PROBLEMA: múltiplas realocações
func coletarIDs(usuarios []Usuario) []int {
var ids []int
for _, u := range usuarios {
ids = append(ids, u.ID) // realoca várias vezes
}
return ids
}
// MELHOR: pré-aloca com capacidade conhecida
func coletarIDsOtimizado(usuarios []Usuario) []int {
ids := make([]int, 0, len(usuarios))
for _, u := range usuarios {
ids = append(ids, u.ID) // zero realocações
}
return ids
}
Maps: pré-alocação
// PROBLEMA: map cresce incrementalmente
func indexar(itens []Item) map[string]Item {
idx := make(map[string]Item) // começa vazio
for _, item := range itens {
idx[item.ID] = item
}
return idx
}
// MELHOR: hint de tamanho reduz rehashing
func indexarOtimizado(itens []Item) map[string]Item {
idx := make(map[string]Item, len(itens))
for _, item := range itens {
idx[item.ID] = item
}
return idx
}
sync.Pool: reutilização de objetos
var poolBuffers = sync.Pool{
New: func() any {
return bytes.NewBuffer(make([]byte, 0, 4096))
},
}
func processarRequisicao(dados []byte) string {
buf := poolBuffers.Get().(*bytes.Buffer)
buf.Reset()
defer poolBuffers.Put(buf)
// usa buf sem alocar novo buffer a cada requisição
buf.Write(dados)
buf.WriteString(" processado")
return buf.String()
}
Benchmark comparativo: antes e depois
package cache_test
import (
"testing"
)
// Versão 1: sem pré-alocação, com concatenação
func buscarChavesV1(m map[string]int) []string {
var chaves []string
for k := range m {
chaves = append(chaves, k)
}
return chaves
}
// Versão 2: com pré-alocação
func buscarChavesV2(m map[string]int) []string {
chaves := make([]string, 0, len(m))
for k := range m {
chaves = append(chaves, k)
}
return chaves
}
// Versão 3: reutilizando slice passado como argumento
func buscarChavesV3(m map[string]int, destino []string) []string {
destino = destino[:0]
for k := range m {
destino = append(destino, k)
}
return destino
}
func setupMap(n int) map[string]int {
m := make(map[string]int, n)
for i := 0; i < n; i++ {
m[fmt.Sprintf("chave_%d", i)] = i
}
return m
}
func BenchmarkBuscarChavesV1(b *testing.B) {
m := setupMap(1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
buscarChavesV1(m)
}
}
func BenchmarkBuscarChavesV2(b *testing.B) {
m := setupMap(1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
buscarChavesV2(m)
}
}
func BenchmarkBuscarChavesV3(b *testing.B) {
m := setupMap(1000)
destino := make([]string, 0, 1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
buscarChavesV3(m, destino)
}
}
benchstat: comparando resultados estatisticamente
go install golang.org/x/perf/cmd/benchstat@latest
# Captura resultados antes da otimização
go test -bench=. -benchmem -count=10 ./... > antes.txt
# Faz a otimização...
# Captura resultados depois
go test -bench=. -benchmem -count=10 ./... > depois.txt
# Compara estatisticamente
benchstat antes.txt depois.txt
Saída:
goos: linux
goarch: amd64
│ antes.txt │ depois.txt │
│ sec/op │ sec/op vs base │
BuscarV1-8 450ns ± 2% 120ns ± 1% -73.33% (p=0.000)
BuscarV2-8 130ns ± 1% 120ns ± 1% -7.69% (n.s.)
│ antes.txt │ depois.txt │
│ B/op │ B/op vs base │
BuscarV1-8 8192 ± 0% 4096 ± 0% -50.00% (p=0.000)
BuscarV2-8 4096 ± 0% 4096 ± 0% ~ (n.s.)
O p=0.000 indica que a diferença é estatisticamente significativa. n.s. (not significant) indica que a diferença pode ser ruído.
Expondo métricas de runtime
package main
import (
"encoding/json"
"net/http"
"runtime"
"time"
)
type MetricasRuntime struct {
Goroutines int `json:"goroutines"`
HeapAllocMB uint64 `json:"heap_alloc_mb"`
HeapSysMB uint64 `json:"heap_sys_mb"`
HeapObjects uint64 `json:"heap_objects"`
NumGC uint32 `json:"num_gc"`
PausaTotalMs uint64 `json:"pausa_total_ms"`
UltimaGCMs int64 `json:"ultima_gc_ms"`
}
func handlerMetricas(w http.ResponseWriter, r *http.Request) {
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
m := MetricasRuntime{
Goroutines: runtime.NumGoroutine(),
HeapAllocMB: stats.HeapAlloc / 1024 / 1024,
HeapSysMB: stats.HeapSys / 1024 / 1024,
HeapObjects: stats.HeapObjects,
NumGC: stats.NumGC,
PausaTotalMs: stats.PauseTotalNs / 1e6,
UltimaGCMs: time.Unix(0, int64(stats.LastGC)).UnixMilli(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(m)
}
Regras práticas de performance em Go
1. Meça antes de otimizar — use benchmarks e pprof
2. Reduza alocações — é a otimização mais impactante em Go
3. Pré-aloque slices e maps quando o tamanho é conhecido
4. Use sync.Pool para objetos frequentemente alocados e descartados
5. Prefira valores a ponteiros para structs pequenas
6. Use strings.Builder para concatenação de strings em loops
7. Evite interface{}/any em hot paths — causa boxing e alocações
8. Leia o escape analysis — go build -gcflags="-m"
9. Use benchstat para comparações estatisticamente válidas
10. O GC é seu amigo — reduzir alocações beneficia automaticamente
Resumo do que foi coberto
Este artigo apresentou profiling e otimização de performance em Go de forma completa: benchmarks com métricas de memória, coleta de perfis de CPU e memória com pprof via HTTP e benchmarks, a interface web do pprof com flamegraph, trace para análise temporal, escape analysis para entender alocações de heap, padrões de otimização — pré-alocação de slices e maps, sync.Pool, preferência por valores — benchstat para comparação estatística e exposição de métricas de runtime. Com este artigo o Módulo 7 está completo.
Referências e leituras complementares
-
Documentação pprof — Referência completa do pacote net/http/pprof. https://pkg.go.dev/net/http/pprof
-
Go Blog: Profiling Go Programs — Tutorial oficial de profiling. https://go.dev/blog/pprof
-
Go Blog: Diagnostics — Visão geral de todas as ferramentas de diagnóstico. https://go.dev/doc/diagnostics
-
benchstat — Ferramenta de comparação estatística de benchmarks. https://pkg.go.dev/golang.org/x/perf/cmd/benchstat
-
Go execution tracer — Documentação do runtime/trace. https://pkg.go.dev/runtime/trace
-
High Performance Go Workshop — Dave Cheney — Workshop completo sobre performance. https://dave.cheney.net/high-performance-go-workshop/gopherchina-2019.html
Próximo artigo: Artigo 44 — Clean Architecture em Go: camadas e dependências**
you asked
Sim