Alocação em Stack: O Segredo Invisível da Performance em Go
Durante muito tempo, eu achei que performance em Go era apenas sobre escolher o melhor algoritmo ou usar goroutines para tudo. Será que o melhor era realmente entregar código rápido? Conforme os sistemas que eu liderava cresciam, especialmente em ambientes de fintech com milhares de transações por segundo, percebi que o buraco era mais embaixo.
Eu já perdi noites analisando picos de latência em APIs. O culpado? Nem sempre era o banco de dados. Muitas vezes, era a pressão absurda sobre o Garbage Collector (GC). E a solução para isso começa em um conceito que poucos dominam, mas agora vou explicar de forma breve: a alocação em Stack vs. Heap.
O Balcão da Cozinha e o Depósito Externo
Para facilitar, imagine que a Stack é o balcão da sua cozinha. É um espaço pequeno, mas tudo o que você coloca lá está à mão e, assim que você termina de cozinhar, o balcão é limpo instantaneamente. Já o Heap é como um depósito nos fundos da casa: cabe muita coisa, mas toda vez que você precisa de algo, tem que caminhar até lá, e alguém (o GC) precisa passar periodicamente para jogar fora o que você não usa mais.
Em Go, quando o compilador decide que uma variável pode ficar na Stack, a performance voa. Quando ela "escapa" para o Heap, o custo de gerenciamento sobe.
O Cenário: Validação de Transações de Cartão
Imagine que você está construindo um validador para uma adquirente. Cada milissegundo economizado aqui evita que a fila de autorização engasgue.
O Jeito que Pesa (Escape Analysis no Heap)
Muitas vezes, por hábito de outras linguagens, tendemos a retornar ponteiros para "economizar memória". Mas em Go, isso pode causar o efeito oposto.
type Transaction struct {
ID string
Amount float64
}
// Ao retornar um ponteiro, o Go entende que esse objeto
// precisa sobreviver fora da função. Ele "escapa" para o Heap.
func NewTransaction(id string, amount float64) *Transaction {
return &Transaction{
ID: id,
Amount: amount,
}
}Neste cenário de fintech, se você processa 10k transações/seg, você acabou de criar 10 mil objetos no Heap que o GC terá que limpar depois. Isso gera as famosas "pausas de GC" que estragam seu SLA.
O Jeito Performático (Alocação em Stack)
Sempre que possível, prefira retornar valores em vez de ponteiros para objetos pequenos e de vida curta.
// Ao retornar o valor direto, os dados ficam na Stack da goroutine
// que chamou a função. Zero trabalho para o GC.
func NewTransaction(id string, amount float64) Transaction {
return Transaction{
ID: id,
Amount: amount,
}
}
Por que isso muda o jogo?
Em sistemas críticos, a previsibilidade é tão importante quanto a velocidade. Quando você favorece a alocação em Stack, você ganha:
- Localidade de cache: O processador acessa a Stack muito mais rápido que o Heap.
- Menos pressão no GC: O coletor de lixo não precisa monitorar o que está na Stack.
- Latência consistente: Você evita aqueles "engasgos" aleatórios no meio do dia.
Como saber se meu código está "escapando"?
Você não precisa adivinhar. O compilador do Go é seu melhor amigo aqui. Use este comando no terminal para ver o que o Go está fazendo por baixo dos panos:
go build -gcflags="-m" main.go
Ele vai te dizer frases como: “... escapes to heap” ou “... moved to heap”. É um exercício de humildade técnica rodar isso e perceber que aquele seu código "esperto" estava, na verdade, pesando no sistema.
Pequenas atitudes para o dia a dia
Se você quer escrever código com mentalidade de performance, comece a observar:
- Evite o uso desnecessário de
interface{}: Interfaces quase sempre forçam a alocação no Heap. - Cuidado com ponteiros em loops: Criar variáveis dentro de um loop e armazenar o ponteiro delas é um convite para o Heap.
- Tamanho importa: Se a struct for gigante, o ponteiro faz sentido. Se for um DTO simples de transação, o valor é seu aliado.
Código não deve ser o mais complexo, deve ser o que melhor respeita os recursos da máquina. Se você começar a olhar para suas alocações hoje, garanto que o seu "eu" de daqui a seis meses (e o seu time de SRE) vai te agradecer muito.
Você já parou para medir quanto do seu tempo de resposta é gasto apenas com o lixo que seu código gera? Talvez o segredo daquela latência chata esteja apenas no modo como você retorna suas structs.
