Go: Análise de desempenho em Go
Última atualização: 2026-08-26
A análise de desempenho não é nenhum mistério — ao usar o pprof, o benchmark e o trace em conjunto, você pode identificar gargalos de CPU, memória e concorrência em seus programas em Go.
Quando o tempo de resposta de uma API em Go passa de 50 ms para 5 s, você adiciona registros de log ao seu código para investigar o problema ou utiliza ferramentas para identificar a causa? Nesta lição, você vai dominar todo o conjunto de ferramentas para análise de desempenho em Go.
1. Você aprenderá
pprofmétodos de inicialização (HTTP / teste / arquivo)- Análise do perfil da CPU das funções que apresentam picos de uso
- Análise do perfil da pilha para alocação de memória
- Analisando problemas de concorrência com o perfil de goroutines
benchmarkteste de desempenho +-benchmemtrace: Rastrear o agendamento de goroutines-raceDetecção de conflito de acesso aos dados
2. A história real de um engenheiro de backend
(1) Problema: o tempo de resposta da API passou de 50 ms para 5 s; levou uma semana de monitoramento para descobrir o que estava acontecendo
Bob é engenheiro de backend na equipe de pagamentos, e sua API vem ficando cada vez mais lenta:
“A interface de pagamento estava funcionando bem há um mês, mas, nesta semana, está demorando 5 segundos para responder. Adicionei 50 linhas de registro de log, usando
fmt.Printlnpara marcar a hora de início e fim de cada função — modifiquei o código 10 vezes e o implementei 10 vezes, mas ainda assim não consegui identificar o problema. Meu chefe me perguntou: ‘Já faz uma semana — qual é exatamente o problema?’”
Suas suspeitas apontam para:
❌ Database too slow? — But queries only take 2ms
❌ Downstream service timeout? — Called it and the response is normal
❌ Network latency? — All in the same datacenter
✅ Actual cause: string concatenation causing massive memory allocation + frequent GC
(2) Solução Go: pprof para depuração precisa
import (
"net/http"
_ "net/http/pprof" // One line to enable pprof
)
func main() {
// pprof endpoints auto-registered at /debug/pprof/
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Business code continues running...
}
Então, Bob saiu correndo:
# Collect 30-second CPU profile
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/profile?seconds=30
# Result: flame graph shows strings.Builder only uses 2% CPU,
# while strings.Join + garbage collection uses 78% CPU!
(3) Benefícios: Adivinhação x Ferramentas
| Método | Tempo | Precisão |
|---|---|---|
fmt.Println registro |
1 semana (várias implantações) | ❌ Suposição |
| Perfil de CPU com o pprof | 30 segundos | ✅ Identificação precisa das funções mais utilizadas |
| Perfil de heap do pprof | 1 segundo | ✅ Detalhes da alocação de memória até os números de linha |
3. Como iniciar o pprof
(1) ▶ Exemplo: Método HTTP (o mais comum)
package main
import (
"fmt"
"log"
"net/http"
_ "net/http/pprof" // Import to auto-register pprof endpoints
"time"
)
func slowFunction() {
// Simulate a slow function
var result string
for i := 0; i < 100000; i++ {
result += fmt.Sprintf("%d ", i) // Bad string concatenation
}
}
func main() {
// Start pprof HTTP service (separate port, not exposed externally)
go func() {
log.Println("pprof listening on :6060")
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Business service
http.HandleFunc("/work", func(w http.ResponseWriter, r *http.Request) {
slowFunction()
fmt.Fprintln(w, "done")
})
log.Println("Business service listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
# pprof endpoints:
# http://localhost:6060/debug/pprof/ — index
# http://localhost:6060/debug/pprof/profile — CPU profile (default 30s)
# http://localhost:6060/debug/pprof/heap — Heap profile
# http://localhost:6060/debug/pprof/goroutine — goroutine info
# http://localhost:6060/debug/pprof/block — block analysis
# http://localhost:6060/debug/pprof/mutex — lock contention analysis
(2) ▶ Exemplo: Métodos de teste (benchmark + pprof)
// string_bench_test.go
package main
import (
"strings"
"testing"
)
// Bad approach: + concatenation
func BenchmarkStringPlus(b *testing.B) {
for i := 0; i < b.N; i++ {
var s string
for j := 0; j < 1000; j++ {
s += "a"
}
}
}
// Good approach: strings.Builder
func BenchmarkStringBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
for j := 0; j < 1000; j++ {
sb.WriteString("a")
}
_ = sb.String()
}
}
// Good approach: pre-allocate
func BenchmarkStringBuilderPrealloc(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
sb.Grow(1000)
for j := 0; j < 1000; j++ {
sb.WriteString("a")
}
_ = sb.String()
}
}
# Run benchmark (view memory allocations)
$ go test -bench=. -benchmem -count=3
# Generate CPU profile
$ go test -bench=. -cpuprofile=cpu.prof -memprofile=mem.prof
# Analyze profile
$ go tool pprof -http=:8081 cpu.prof
4. Perfil da CPU
(1) ▶ Exemplo: Identificação de pontos de sobrecarga da CPU
package main
import (
"log"
"net/http"
_ "net/http/pprof"
"strings"
)
func heavyCPU() string {
var sb strings.Builder
for i := 0; i < 10000; i++ {
sb.WriteString("hello")
sb.WriteString(" ")
sb.WriteString("world")
sb.WriteString("\n")
}
return sb.String()
}
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
http.HandleFunc("/cpu", func(w http.ResponseWriter, r *http.Request) {
result := heavyCPU()
w.Write([]byte(result[:100]))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
# Collect CPU profile (access /cpu endpoint multiple times within 30 seconds)
$ go tool pprof -http=:8081 http://localhost:6060/debug/pprof/profile?seconds=30
# CLI mode
$ go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
(pprof) top # Show Top 10 hot functions
(pprof) list main # View per-line time in the main package
(pprof) web # Open visualization in browser
(2) Como interpretar a saída do comando pprof top
(pprof) top
Showing nodes accounting for 4.56s, 82.31% of 5.54s total
Dropped 28 nodes (cum <= 0.03s)
flat flat% sum% cum cum%
2.34s 42.24% 42.24% 2.34s 42.24% runtime.memmove
1.12s 20.22% 62.46% 1.12s 20.22% runtime.mallocgc
0.56s 10.11% 72.57% 0.56s 10.11% strings.(*Builder).copy
...
| Coluna | Significado |
|---|---|
flat |
Tempo gasto pela própria função atual |
flat% |
Porcentagem do tempo total gasto em “flat” |
sum% |
Porcentagem acumulada |
cum |
Tempo consumido pela função atual e por todas as subfunções que ela chama |
cum% |
Porcentagem do tempo total representada pelo cum |
flat são “lentas por si só” (pontos críticos), enquanto as funções com um valor alto de cum, mas baixo de flat, são “lentas devido a chamadas” (problemas de gerenciamento). Otimize primeiro as funções com os valores mais altos de flat — isso trará os resultados mais rápidos.
5. Perfil da pilha
(1) ▶ Exemplo: Identificação de vazamentos de memória
package main
import (
"log"
"net/http"
_ "net/http/pprof"
)
var leak []string // Global variable, never garbage collected
func memoryLeak() {
// Appends 10000 entries on each call, never clears
for i := 0; i < 10000; i++ {
leak = append(leak, "leaked string data")
}
}
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
http.HandleFunc("/leak", func(w http.ResponseWriter, r *http.Request) {
memoryLeak()
w.Write([]byte("leaked"))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
# Collect heap profile (view current memory allocation)
$ go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap
# View functions with the most allocations
$ go tool pprof -alloc_space http://localhost:6060/debug/pprof/heap
(pprof) top
(pprof) list main.memoryLeak
(2) Modo de visualização da pilha
# Four viewing modes:
-inuse_space # Currently in-use memory (default)
-inuse_objects # Currently in-use object count
-alloc_space # Total allocated memory
-alloc_objects # Total allocated object count
# Use alloc_space to find leaks (see who allocates the most)
$ go tool pprof -alloc_space http://localhost:6060/debug/pprof/heap
| Modo | Finalidade |
|---|---|
inuse_space |
Uso atual da memória (resultado final da detecção de vazamentos) |
inuse_objects |
Número de objetos atuais (para localizar um grande número de objetos pequenos) |
alloc_space |
Alocação total (para identificar a causa principal das frequentes GCs) |
alloc_objects |
Número total de alocações (para identificar objetos de curta duração) |
6. Perfil da goroutine
(1) ▶ Exemplo: Detecção de vazamento de goroutines
# View goroutine count and status
$ go tool pprof http://localhost:6060/debug/pprof/goroutine
# View goroutine stack trace (text)
$ curl http://localhost:6060/debug/pprof/goroutine?debug=2
package main
import (
"fmt"
"log"
"net/http"
_ "net/http/pprof"
"time"
)
func leakyGoroutine() {
ch := make(chan int)
go func() {
// This goroutine will never exit
val := <-ch // Blocks forever
fmt.Println(val)
}()
// ch will never receive data
}
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
http.HandleFunc("/leak", func(w http.ResponseWriter, r *http.Request) {
leakyGoroutine()
w.Write([]byte("leaked"))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
$ curl http://localhost:6060/debug/pprof/goroutine?debug=2
# The output shows each goroutine's stack trace:
# goroutine 5 [chan receive]:
# main.leakyGoroutine.func1()
# /app/main.go:14
# If you see many [chan receive] with no corresponding sender → leak
7. Benchmark e rastreamento
(1) ▶ Exemplo: Benchmark + -benchmem
// bench_test.go
package main
import (
"encoding/json"
"testing"
)
type Data struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// Benchmark: JSON serialization performance
func BenchmarkJSONMarshal(b *testing.B) {
data := Data{ID: 1, Name: "Alice", Email: "alice@example.com"}
for i := 0; i < b.N; i++ {
_, err := json.Marshal(data)
if err != nil {
b.Fatal(err)
}
}
}
// Benchmark: JSON serialization + pre-allocated buffer
func BenchmarkJSONMarshalBuffer(b *testing.B) {
data := Data{ID: 1, Name: "Alice", Email: "alice@example.com"}
buf := make([]byte, 0, 256)
for i := 0; i < b.N; i++ {
buf = buf[:0]
result, err := json.Marshal(data)
if err != nil {
b.Fatal(err)
}
buf = append(buf, result...)
}
}
$ go test -bench=. -benchmem -count=5 ./...
BenchmarkJSONMarshal-8 10000000 156.2 ns/op 48 B/op 1 allocs/op
BenchmarkJSONMarshalBuffer-8 10000000 158.1 ns/op 48 B/op 1 allocs/op
(2) ▶ Exemplo: Rastreamento
package main
import (
"fmt"
"os"
"runtime/trace"
"sync"
)
func main() {
// Create trace file
f, _ := os.Create("trace.out")
defer f.Close()
// Start trace
trace.Start(f)
defer trace.Stop()
// Run code under test
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
result := fibonacci(30)
fmt.Printf("Worker %d: %d\n", id, result)
}(i)
}
wg.Wait()
}
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
# After generating the trace file, view it in a browser
$ go tool trace trace.out
# Opens the browser, showing:
# - Goroutine analysis: how long each goroutine ran
# - Scheduling latency: when goroutines were scheduled
# - Network blocking: what goroutines are waiting for
# - System calls: when GC ran
8. Exemplo completo: Identificando “Resposta lenta de 500 ms”
(1) ▶ Exemplo: Demonstração completa de depuração
// debug_demo.go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
_ "net/http/pprof"
"strings"
)
// ---------- Slow API ----------
type UserResponse struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Bio string `json:"bio"`
}
// Bad version: string concatenation + heavy allocation
func generateUserJSON(userID int) []byte {
var bio strings.Builder
// Simulate generating a large amount of text
for i := 0; i < 1000; i++ {
bio.WriteString(fmt.Sprintf("Line %d: User data for ID %d with some additional info\n", i, userID))
}
resp := UserResponse{
ID: userID,
Name: fmt.Sprintf("User_%d", userID),
Email: fmt.Sprintf("user%d@example.com", userID),
Bio: bio.String(),
}
data, _ := json.Marshal(resp)
return data
}
// Optimized version: pre-allocation + reduced formatting
func generateUserJSONOptimized(userID int) []byte {
// Pre-allocate buffer
var bio strings.Builder
bio.Grow(50000) // Estimated size
for i := 0; i < 1000; i++ {
bio.WriteString("Line ")
bio.WriteString(fmt.Sprintf("%d", i)) // Can be further optimized with strconv.Itoa
bio.WriteString(": User data for ID ")
bio.WriteString(fmt.Sprintf("%d", userID))
bio.WriteString(" with some additional info\n")
}
resp := UserResponse{
ID: userID,
Name: "User_" + fmt.Sprintf("%d", userID),
Email: fmt.Sprintf("user%d@example.com", userID),
Bio: bio.String(),
}
data, _ := json.Marshal(resp)
return data
}
// ---------- Analysis workflow ----------
/*
Debugging workflow:
(1) Step 1: Start pprof HTTP server
go run main.go (automatically starts pprof on :6060)
(2) Step 2: Load test
# In another terminal, send continuous requests
while true; do curl http://localhost:8080/user/1 > /dev/null; done
(3) Step 3: Collect CPU profile
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/profile?seconds=30
(4) Step 4: View flame graph in browser
- Look for the widest color blocks → hot functions
- If you see runtime.memmove / runtime.mallocgc → excessive memory allocation
- Click main.generateUserJSON → view per-line code timing
(5) Step 5: View Heap profile
go tool pprof -alloc_space http://localhost:6060/debug/pprof/heap
(pprof) top
*/
func main() {
// pprof
go func() {
log.Println("pprof on :6060")
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Business endpoint
http.HandleFunc("/user/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var userID int
fmt.Sscanf(id, "%d", &userID)
data := generateUserJSON(userID)
w.Header().Set("Content-Type", "application/json")
w.Write(data)
})
log.Println("Service listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
flowchart TD
A[API slow response] --> B{Problem type?}
B -->|High CPU| C[pprof CPU profile]
B -->|High memory| D[pprof Heap profile]
B -->|Many goroutines| E[pprof goroutine profile]
B -->|Scheduling latency| F[go tool trace]
C --> C1[View flame graph]
C1 --> C2{Hot function?}
C2 -->|runtime.memmove| G[Reduce memory allocation]
C2 -->|Business function| H[Optimize algorithm / add cache]
D --> D1[View alloc_space]
D1 --> D2{Who allocates the most?}
D2 -->|strings.Builder| I[Pre-allocate with Grow]
D2 -->|Temporary objects| J[Use sync.Pool]
E --> E1[View goroutine stack]
E1 --> E2{Goroutine status?}
E2 -->|chan receive blocked| K[Check channel sender]
E2 -->|IO wait| L[Check connection pool]
F --> F1[View goroutine analysis]
F1 --> F2{Scheduling latency?}
F2 -->|GC pause| M[Reduce memory allocation]
F2 -->|System calls| N[Optimize IO operations]
pprof para coletar dados primeiro e, em seguida, use esses dados para tomar decisões. Os gargalos de desempenho mais comuns no Go são a alocação excessiva de memória (alta pressão do GC) e a concatenação ineficiente de strings.
❓ Perguntas Frequentes
P: Como faço para iniciar o pprof? R: Existem duas maneiras: (1) Método HTTP:
import _ "net/http/pprof", em seguida, inicie o serviço HTTP; o endpoint é registrado automaticamente em/debug/pprof/; (2) Modo de teste:go test -cpuprofile=cpu.prof -memprofile=mem.prof. Em um ambiente de produção, use o método HTTP por meio de uma porta dedicada (não exposta ao público).
P: Qual é a diferença entre um perfil de CPU e um perfil de heap? R: Um perfil de CPU analisa “quais funções a CPU está executando no momento” (amostragem cronometrada) e é usado para identificar pontos de congestionamento da CPU. Um perfil de heap analisa “quais objetos foram alocados na memória” e é usado para identificar vazamentos de memória e pressão do GC. Esses são dois tipos distintos de perfis e podem ser coletados simultaneamente.
P: O que devo observar em um perfil de goroutines? R: Verifique o número e o status das goroutines. Use
curl /debug/pprof/goroutine?debug=2para visualizar o rastreamento de pilha de cada goroutine. Um grande número de goroutines no estado [chan receive] pode indicar um vazamento de canal. Um grande número de goroutines no estado [IO wait] pode indicar um pool de conexões insuficiente.
P: Como devo interpretar os resultados da opção
-benchmemno benchmark? R: Há três colunas:ns/op(tempo por operação),B/op(bytes alocados por operação) eallocs/op(número de alocações por operação). Objetivo da otimização: reduzirallocs/op(número de alocações), pois o tempo de GC está relacionado ao número de objetos.
P: Qual é a diferença entre o trace e o pprof? R: O pprof captura um “instantâneo” (amostragem) para responder à pergunta “onde está o gargalo?”; o trace grava um “vídeo” (fluxo de eventos) para responder à pergunta “por que está lento?”. O trace fornece uma linha do tempo completa do agendamento das goroutines — quando uma goroutine é executada, quando ela fica bloqueada e quando o GC faz uma pausa. Primeiro, use o pprof para identificar a área problemática; depois, use o trace para analisar a causa raiz.
P: Como utilizo o detector de conflitos de acesso? R:
go run -race main.goougo test -race ./.... O detector de conflitos de acesso detecta conflitos de acesso em tempo de execução — leituras e gravações simultâneas na mesma variável (com pelo menos uma gravação) acionarão um aviso. Recomenda-se sempre habilitar-raceem CI/CD, mas isso diminui significativamente a velocidade de execução (5 a 20 vezes), portanto, não o habilite em ambientes de produção.
P: Quais são alguns gargalos comuns de desempenho no Go? R: (1) Usar
+para concatenação de strings em vez destrings.Builder; (2) Esquecer de pré-alocar tamanhos de slices/maps; (3) Serialização/desserialização frequente de JSON; (4) Vazamentos de goroutines que fazem com que recursos permaneçam não liberados; (5) Bloqueios devido ao uso inadequado de canais; (6) Conflitos intensos de bloqueio. Use o pprof para identificar esses problemas e otimizá-los um por um.
📖 Resumo
- Inicialização do pprof:
import _ "net/http/pprof"+ ponto de extremidade HTTP - Perfil da CPU:
/debug/pprof/profile?seconds=30 - Perfil de heap:
/debug/pprof/heap(quatro modos) - Perfil da goroutine:
/debug/pprof/goroutine?debug=2 - referência:
go test -bench=. -benchmem -cpuprofile=... - rastreamento:
go tool trace trace.out(para visualizar a latência de agendamento) - detector de corrida:
go test -race ./... - Processo otimizado: Medir → Identificar → Otimizar — sem necessidade de suposições
📝 Exercícios
-
Básico (Dificuldade ⭐): Escreva um programa com problemas de desempenho (envolvendo um grande número de concatenações de strings usando
+) e habilite o endpoint HTTP do pprof. Executego tool pprof -http=:8081para visualizar o perfil da CPU e identificar os pontos críticos. Em seguida, otimize o código usandostrings.Buildere compare os perfis da CPU antes e depois da alteração. -
Avançado (Dificuldade ⭐⭐): Analise o desempenho da serialização JSON usando benchmark e pprof. Requisitos: (1) Crie uma estrutura contendo 100 campos; (2) Compare o desempenho de
json.Marshalejson.Encoder; (3) Use-benchmempara visualizar a alocação de memória; (4) Use-cpuprofilepara gerar um perfil e analise os pontos críticos comgo tool pprof. -
Desafio (Dificuldade ⭐⭐⭐): Diagnostique e corrija um programa com vazamentos de memória. Você receberá um trecho de código em Go que contém vazamentos de memória (vazamentos de goroutine + vazamentos de slice). Requisitos: (1) Use a análise de heap do pprof para localizar a origem dos vazamentos; (2) Use um perfil de goroutine para confirmar o número de vazamentos; (3) Use o
-racepara detectar problemas de concorrência; (4) Após corrigir todos os problemas, use o pprof para verificar se não há mais vazamentos; (5) Elabore um relatório de diagnóstico completo.