Otimização de desempenho de aplicações Go: na prática
Go (Golang) é famoso por seu desempenho "pronto para uso", mas qualquer aplicação real pode, com o tempo, encontrar gargalos: consultas lentas, alto consumo de memória ou operação ineficiente de goroutines. Neste artigo, abordaremos métodos práticos de otimização de aplicações Go, desde profiling até ajustes finos no garbage collector. Você obterá técnicas prontas e exemplos de código que ajudarão a acelerar seu projeto.
1. Profiling: encontrando gargalos
Antes de otimizar qualquer coisa, é preciso entender o que está causando lentidão. Em Go, existe o pacote integrado runtime/pprof e a ferramenta go tool pprof. O profiling pode ser de três tipos:
- Perfil de CPU — mostra quais funções consomem mais tempo de processador.
- Perfil de Heap — análise de alocação de memória.
- Perfil de Goroutines — quantidade e estado das goroutines ativas.
Adicione um endpoint HTTP simples para coletar perfis:
package main
import ( "net/http" _ "net/http/pprof")
func main() { // Habilita profiling no endereço /debug/pprof/ http.ListenAndServe(":8080", nil)}Em seguida, execute a aplicação e colete os dados:
go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30Após a coleta, você entrará em um console interativo. O comando top mostrará as funções mais "quentes", e web exibirá um grafo visual de chamadas. Este é o primeiro passo para entender onde ocorrem as perdas.
2. Gerenciamento de memória: escape analysis e alocações
Um dos problemas comuns são alocações excessivas no heap. Go usa escape analysis — o compilador decide se aloca uma variável na stack ou no heap. Se uma variável "escapa" (escape) para o heap, isso aumenta a carga no garbage collector.
Veja um exemplo:
// Ruim: retornando um ponteiro para uma variável localfunc createUser() *User { u := User{Name: "Alice"} return &u // escape para o heap}
// Bom: passando a struct por valorfunc processUser(u User) string { return u.Name}Para verificar quais variáveis escapam, use a flag do compilador:
go build -gcflags="-m" main.goVocê verá linhas como: moved to heap: u. Tente minimizar esses casos. Por exemplo, em vez de retornar um ponteiro, retorne o valor (se a struct for pequena) ou use pools de objetos (sync.Pool).
sync.Pool é uma excelente ferramenta para objetos temporários que são frequentemente criados e destruídos:
var bufferPool = sync.Pool{ New: func() interface{} { return new(bytes.Buffer) },}
func handleRequest() { buf := bufferPool.Get().(*bytes.Buffer) defer bufferPool.Put(buf) buf.Reset() // trabalho com buf}Isso reduz o número de alocações e diminui as pausas do GC.
3. Concorrência: goroutines e canais sem dor
Go é famoso por suas goroutines leves, mas o uso incorreto pode prejudicar o desempenho. Aqui estão três dicas práticas:
3.1. Limite o número de goroutines
Criar um milhão de goroutines é uma má ideia. Cada goroutine ocupa pelo menos 2-4 KB de stack, além da sobrecarga do escalonador. Use um worker pool com um canal bufferizado:
func workerPool(jobs <-chan int, results chan<- int, workers int) { var wg sync.WaitGroup for i := 0; i < workers; i++ { wg.Add(1) go func() { defer wg.Done() for job := range jobs { results <- process(job) } }() } wg.Wait() close(r