Memória e coletor de lixo em Go: como não enlouquecer com o GC
\nVocê já se perguntou para onde vai a memória na sua aplicação Go? Especialmente quando ela começa a consumir gigabytes, e você está apenas analisando JSON? Familiar? Bem-vindo ao mundo do coletor de lixo (GC) em Go — o mecanismo misterioso que ou salva seu projeto ou o transforma em uma plataforma lenta. Hoje vamos analisar como a memória funciona em Go, como o GC decide que é hora de limpar e como não cair na armadilha da "espera pelo STW".
\nSe você programa em Go há pelo menos alguns meses, já enfrentou isso: pausas repentinas, aumento no consumo de memória, gc pause misteriosos nos perfis. Vamos dar uma olhada sob o capô e entender como gerenciar a memória em Go de forma consciente.
Como a memória é organizada em Go: heap e stack
\nAntes de falar sobre o coletor de lixo, vamos entender onde os dados realmente vivem em Go. Ao contrário de linguagens como C, onde você é responsável por malloc/free, o Go usa gerenciamento automático de memória. Mas isso não significa que tudo vai para um monte.
\nEm Go, existem duas regiões principais de memória:
\n- \n
- Stack (pilha) — memória rápida e gerenciada automaticamente para variáveis locais. Cada goroutine tem sua própria stack (começa com 2 KB e cresce dinamicamente). \n
- Heap (monte) — memória mais lenta, mas flexível, para dados que "sobrevivem" à chamada de uma função. \n
A questão principal: o que vai para o heap e o que fica na stack? Isso é decidido pelo escape analysis — um mecanismo inteligente do compilador Go. Ele analisa se uma variável "escapa" dos limites da função. Se sim, ela vai para o heap. Se não, permanece na stack, e o GC nem fica sabendo dela.
\nExemplo:
\npackage main\n\nfunc createPointer() *int {\n x := 42\n return &x // x "escapa" — vai para o heap\n}\n\nfunc noEscape() int {\n x := 42\n return x // x permanece na stack\n}\n\nNo primeiro caso, o compilador vê que o ponteiro para x é retornado para fora e aloca a variável no heap. No segundo, tudo é simples e rápido. Isso é o gerenciamento de memória em Go no micronível.
Coletor de lixo em Go: como ele funciona
\nAgora, sobre o protagonista — o coletor de lixo (GC). Em Go, é usado um coletor paralelo, tricolor, não incremental com o algoritmo "mark-and-sweep". Parece assustador? Na verdade, tudo é lógico.
\nO GC em Go passa por três fases:
\n- \n
- Mark Setup (STW) — pausa curta para ativar a write barrier. Geralmente leva de 10 a 30 microssegundos. \n
- Marking (Concurrent) — trabalho principal: o GC percorre o grafo de objetos, marcando os vivos. Executado em paralelo com seu código. \n
- Sweep (Concurrent + STW) — limpeza de objetos mortos. Feito parcialmente em paralelo, mas a sincronização final pode causar uma pausa curta. \n
A principal característica do GC em Go é que ele não faz desfragmentação. Isso significa que, após a coleta, a memória pode ficar fragmentada, mas o Go usa mapas de bits para rastrear blocos livres, tornando a alocação de memória rápida.
\nUm exemplo do funcionamento do GC pode ser visto nos logs:
\nGODEBUG=gctrace=1 go run main.go\n\ngc 1 @0.013s 4%: 0.031+0.42+0.017 ms clock, 0.25+0.10/0.42/0.0+0.13 ms cpu, 4->4->2 MB, 5 MB goal, 8 P\n\nAqui: 0.031+0.42+0.017 ms — são STW + Mark + STW. 4->4->2 MB — tamanho do heap antes, após o mark e após o sweep.
Por que o GC fica lento: STW e pausas
\nO principal inimigo do desempenho é o STW (Stop The World). Embora o Go tente minimizar as pausas, elas ainda acontecem. Especialmente se:
\n- \n
- Você tem muitos ponteiros (o GC precisa percorrer todos os objetos) \n
- O heap é grande (mais objetos = marking mais demorado) \n
- Você cria muito lixo (high allocation rate) \n