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á


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.Println para 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:

TEXT 📖 Somente leitura
❌ 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

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

BASH
# 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)

GO
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))
}
▶ Experimente
BASH
# 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)

GO
// 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()
    }
}
▶ Experimente
BASH
# 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

GO
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))
}
▶ Experimente
BASH
# 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

TEXT 📖 Somente leitura
(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
💡 Dica: As funções com um valor alto de 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

GO
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))
}
▶ Experimente
BASH
# 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

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

BASH
# 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
GO
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))
}
BASH
$ 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

GO
// 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...)
    }
}
▶ Experimente
BASH
$ 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

GO
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)
}
▶ Experimente
BASH
# 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

GO 📖 Somente leitura
// 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))
}
78 linhas de lógica (limite de 40, somente leitura)
100%
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]
💡 Dica: Uma abordagem em três etapas para otimização de desempenho: medir → identificar → otimizar. Não tente adivinhar onde estão os gargalos — execute o 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=2 para 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 -benchmem no benchmark? R: Há três colunas: ns/op (tempo por operação), B/op (bytes alocados por operação) e allocs/op (número de alocações por operação). Objetivo da otimização: reduzir allocs/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.go ou go 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 -race em 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 de strings.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


📝 Exercícios

  1. 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. Execute go tool pprof -http=:8081 para visualizar o perfil da CPU e identificar os pontos críticos. Em seguida, otimize o código usando strings.Builder e compare os perfis da CPU antes e depois da alteração.

  2. 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.Marshal e json.Encoder; (3) Use -benchmem para visualizar a alocação de memória; (4) Use -cpuprofile para gerar um perfil e analise os pontos críticos com go tool pprof.

  3. 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 -race para 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%