Introdução: por que a performance de aplicações Go é importante
Go (Golang) é conhecido por sua velocidade e eficiência, mas mesmo nesta linguagem é possível escrever código lento. A otimização de performance não é uma micro-otimização prematura, mas sim a aplicação consciente de best practices que tornam sua aplicação rápida, escalável e econômica em recursos. Para desenvolvedores iniciantes em Go, entender os fundamentos de profiling e otimização é um passo fundamental para criar serviços prontos para produção.
Neste artigo, vamos explorar quatro abordagens práticas de otimização que você pode aplicar hoje mesmo: desde a configuração do garbage collector até o trabalho correto com goroutines.
1. Profiling: encontre os gargalos antes de otimizar
Antes de mudar qualquer coisa, é preciso entender onde exatamente a performance está sendo perdida. O Go fornece o pacote nativo pprof para profiling de CPU, memória, bloqueios e goroutines.
Iniciando o profiling de CPU
Adicione ao código a inicialização do profiler:
package main
import ( "os" "runtime/pprof")
func main() { f, _ := os.Create("cpu.prof") pprof.StartCPUProfile(f) defer pprof.StopCPUProfile()
// Seu código heavyFunction()}Após executar o programa, execute no terminal:
go tool pprof -http=:8080 cpu.profNo navegador, será aberta uma visualização onde você verá quais funções consomem mais tempo de CPU. Este é seu primeiro passo para uma otimização consciente.
Profiling de memória
Para encontrar vazamentos e alocações excessivas de memória, use:
import "runtime/pprof"
func main() { f, _ := os.Create("mem.prof") defer f.Close()
// Após a execução do código runtime.GC() pprof.WriteHeapProfile(f)}Analise o resultado através do go tool pprof e preste atenção às funções com grande número de alocações.
2. Gerenciamento de memória: garbage collector e alocações
O Go usa um garbage collector (GC) eficiente, mas seu funcionamento pode desacelerar a aplicação em caso de alocações frequentes. Veja como minimizar a carga.
Redução do número de alocações
Evite criar objetos temporários em loops críticos. Compare as duas abordagens:
// Ruim: nova alocação a cada iteraçãofunc sumBad(nums []int) int { total := 0 for _, n := range nums { total += n _ = make([]byte, 1024) // alocação desnecessária } return total}
// Bom: sem alocações desnecessáriasfunc sumGood(nums []int) int { total := 0 for _, n := range nums { total += n } return total}Configuração do GC
Por padrão, o GC é acionado quando o tamanho do heap dobra em relação ao ciclo anterior. Você pode controlar isso através da variável de ambiente GOGC:
GOGC=100 ./myapp // Comportamento padrãoGOGC=200 ./myapp // Menos ciclos de GC, mas mais memóriaGOGC=off ./myapp // Desligar o GC (apenas para tarefas de curta duração)Para sistemas em tempo real, sensíveis a atrasos, aumente GOGC para 200-400.
3. Concorrência: goroutines e channels sem dor
O Go se orgulha de seu modelo de concorrência, mas o uso incorreto de goroutines pode levar à queda de performance.
Pool de goroutines em vez de criação ilimitada
Criar 100.000 goroutines é uma má ideia. Use channels com buffer e um worker pool:
func workerPool(jobs []int, numWorkers int) { jobsChan := make(chan int, len(jobs)) results := make(chan int, len(jobs))
// Inicia um número fixo de workers for w := 0; w < numWorkers; w++ { go worker(jobsChan, results) }
// Envia as tarefas for _, j := range jobs { jobsChan