O que é o Garbage Collector do Go
O garbage collector (GC) e o componente da runtime do Go responsável por liberar automaticamente a memoria que seu programa não usa mais. Sem ele, você teria que fazer malloc e free manual como em C. Com ele, você escreve código e a linguagem cuida da memoria por você.
O GC do Go sempre foi conhecido por dois pontos: pausas curtas (baixa latência) e bom desempenho geral. Mas nas versões recentes, especialmente a partir do Go 1.19 com o GC GOEXPERIMENT e nas versões 1.21 em diante, a equipe fez mudanças profundas na forma como o GC percorre o heap, reduzindo ainda mais as pausas e tornando o comportamento mais previsível.
Para um desenvolvedor Go, entender como o GC funciona não e só curiosidade académica. Saber o que acontece por baixo dos panos ajuda a escrever código que gera menos pressão no GC, evita alocações desnecessárias e resulta em aplicações mais rápidas e baratas de rodar.
Como o novo GC percorre o heap
O GC do Go usa um algoritmo chamado tri-color mark-and-sweep concorrente. Concorrente significa que ele roda ao mesmo tempo que o seu programa, em vez de parar tudo (stop-the-world) para fazer a coleta. Isso e o que garante as pausas curtas.
O algoritmo divide todos os objetos na memoria em três categorias durante a coleta:
- Branco: objeto ainda não visitado. No inicio da coleta, tudo começa branco.
- Cinza: objeto foi encontrado e esta na fila para ser processado. Os filhos dele ainda não foram visitados.
- Preto: objeto e todos os seus filhos foram visitados. Ele é considerado vivo.
O GC começa pelos objetos raiz (variáveis globais, stack das goroutines), os coloca em cinza, depois processa cada cinza: visita seus filhos (os coloca em cinza), e marca o objeto atual como preto. Ao final, tudo que ficou branco e lixo e pode ser liberado.
Você pode ver o GC em ação com a variável de ambiente GODEBUG=gctrace=1. Cada linha de saída mostra quando uma coleta aconteceu, quanto tempo durou e quanta memoria foi liberada.
Principais mudanças nas versões recentes
A grande mudança das versões recentes foi na forma como o GC divide o trabalho entre a thread dedicada ao GC e as goroutines da aplicação. Antes, as goroutines podiam ser recrutadas para ajudar o GC quando ele estava sobrecarregado, o que causava picos de latência imprevistos.
O novo design separou melhor as responsabilidades e reduziu a quantidade de trabalho que as goroutines da aplicação precisam fazer para o GC. O resultado prático: pausas mais curtas e mais consistentes, especialmente em aplicações com alta taxa de alocação.
- Write barriers mais eficientes: o mecanismo que garante que objetos novos criados durante a coleta não sejam perdidos ficou mais leve.
- Sweeping preguiçoso: a limpeza da memoria liberada acontece de forma mais incremental, distribuindo o custo ao longo do tempo.
- Melhor heurística de trigger: o GC inicia uma nova coleta no momento certo, nem cedo demais (desperdiçando CPU) nem tarde demais (deixando o heap crescer muito).
Como começar: ferramentas para observar o GC
Você não precisa fazer nada para usar o novo GC. Ele é automático. Mas para observar e ajustar o comportamento, você tem algumas ferramentas:
# Ver traces do GC em tempo real
GODEBUG=gctrace=1 ./sua-aplicação
# Saída típica de cada linha:
# gc 1 @0.012s 2%: 0.021+2.1+0.011 ms clock, ...
# gc 1 = ciclo de GC número 1
# @0.012s = tempo desde o inicio do programa
# 2% = porcentagem do tempo em GC
# 0.021+2.1+0.011 = fases: STW sweep termination + marcação concorrente + STW mark termination# Ajustar o alvo de uso de memoria (GOGC)
# Padrão: 100 (trigger quando heap dobra de tamanho)
GOGC=200 ./sua-aplicação # GC menos frequente, mais memoria usada
GOGC=50 ./sua-aplicação # GC mais frequente, menos memoria
# Go 1.19+: limite absoluto de memoria
GOMEMLIMIT=512MiB ./sua-aplicação # nunca usar mais que 512MBO GOMEMLIMIT introducido no Go 1.19 e particularmente útil em containers. Em vez de o GC adivinhar quando acionar a coleta, você diz explicitamente o limite máximo de memoria que o programa pode usar, e o GC se ajusta para respeitar esse limite.
Exemplo prático: medindo o impacto do GC
Para ver o efeito real na sua aplicação, o pacote runtime expõe métricas detalhadas:
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
// Criar pressão no GC com muitas alocações pequenas
for i := 0; i < 1000; i++ {
_ = make([]byte, 1024)
}
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
fmt.Printf("Ciclos de GC: %d\n", stats.NumGC)
fmt.Printf("Pausa total (ms): %.2f\n", float64(stats.PauseTotalNs)/1e6)
fmt.Printf("Pausa máxima (ms): %.2f\n", float64(stats.PauseNs[(stats.NumGC+255)%256])/1e6)
fmt.Printf("Heap em uso (MB): %.2f\n", float64(stats.HeapInuse)/1024/1024)
fmt.Printf("Heap liberado (MB): %.2f\n", float64(stats.HeapReleased)/1024/1024)
time.Sleep(100 * time.Millisecond) // dar tempo para GC rodar
}Para benchmarks mais precisos, use o pprof com o perfil de heap. Ele mostra quais funções estão alocando mais memoria e são as melhores candidatas para otimização.
Use sync.Pool para reutilizar objetos de vida curta que são criados e descartados frequentemente (como buffers de leitura). Isso reduz drasticamente a pressão no GC sem precisar mudar a lógica do seu código.
Comparação com GCs de outras linguagens
Cada linguagem tem sua abordagem para o GC, com tradeoffs diferentes. Comparar ajuda a entender por que o Go fez as escolhas que fez.
Java (ZGC/G1GC): pausas muito curtas também, mas a JVM e mais pesada em memoria. O GC do Java tem mais parâmetros de tuning, o que da mais controle mas também mais complexidade.
Python: usa reference counting + GC cíclico. O reference counting e imediato (objeto morre assim que ninguém mais aponta para ele), mas ciclos de referência precisam do GC separado. Mais previsível para objetos simples, mas com overhead constante por objeto.
Rust: não tem GC. O compilador garante segurança de memoria via sistema de ownership em tempo de compilação. Zero overhead em runtime, mas complexidade de programação maior.
O Go ficou num ponto médio intencional: sem o overhead constante do reference counting do Python, sem a complexidade do ownership do Rust, com pausas menores que versões antigas da JVM. Para a maioria dos serviços web e de infra, e o tradeoff certo.
GOGC=off desabilita completamente o GC. Isso pode fazer sentido em programas de vida muito curta (CLIs), mas em serviços de longa duração vai encher a memoria até o processo ser morto pelo OS.
Pontos positivos e limitações
O GC do Go e genuinamente bom. As pausas são tipicamente abaixo de 1ms em aplicações bem escritas, e o modelo e simples o suficiente para que a maioria dos desenvolvedores nunca precise pensar nele explicitamente.
A principal limitação ainda e o throughput: linguagens sem GC como C, C++ e Rust conseguem processar mais operações por segundo porque não tem overhead de coleta. Para aplicações onde latência ultra-baixa e o principal requisito, o GC do Go pode ser um gargalo.
- Positivos: pausas curtas, configuração simples (GOGC e GOMEMLIMIT), sem memory leaks por referência circular, modelo de memoria previsível.
- Limitações: overhead de throughput comparado a linguagens sem GC, tuning pode ser necessário em cargas extremas, alocações frequentes de objetos pequenos ainda são caras.
Casos de uso reais
Serviços de API de alta concorrência: Go com goroutines e o GC de baixa latência e uma combinação natural. Vários serviços da Cloudflare, Uber e Twitch rodam em Go exatamente por isso.
Ferramentas de linha de comando: CLIs Go iniciam rápido e o GC raramente e um problema (vida curta do processo). Docker, Kubernetes kubectl, Terraform e todos escritos em Go.
Proxies e gateways: Caddy, Traefik e outros proxies em Go se beneficiam do GC previsível para manter latências baixas mesmo sob carga alta.
Workers de processamento em batch: para workers que processam filas (Kafka, RabbitMQ), o GC do Go permite ajustar GOMEMLIMIT para o container e deixar o sistema se auto-regular sem intervenção manual.
Dicas e boas práticas
Defina GOMEMLIMIT como 90% do limite de memoria do seu container. Isso evita que o processo seja morto pelo OOM killer do Kubernetes enquanto ainda deixa espaço para o overhead da runtime.
Prefira slices pre-alocados com capacidade definida (make([]T, 0, cap)) em vez de append sem capacidade inicial. Evita realocações desnecessárias que geram pressão no GC.
Closures que capturam variáveis grandes fazem essas variáveis escaparem para o heap mesmo que pudessem ficar na stack. Use o compilador com -gcflags='-m' para ver o que esta escapando para o heap no seu código.
O pacote golang.org/x/perf/benchstat e ideal para comparar resultados de benchmark antes e depois de uma otimização. Ele calcula significância estatística e evita conclusões erradas por variação normal.
Vale a pena estudar o GC do Go
Para a maioria dos desenvolvedores Go, o GC e transparente o suficiente para ignorar no dia a dia. Mas quando a latência importa, saber como ele funciona faz toda a diferença.
Entender o tri-color mark-and-sweep, saber que GOMEMLIMIT existe e usar GODEBUG=gctrace=1 para observar o comportamento real são habilidades que diferenciam um dev Go intermediário de um sénior. Não e necessário memorizar todos os detalhes, mas saber onde olhar quando houver problema de performance e essencial.
O novo GC e uma melhoria real e tangível. Se você tem uma aplicação Go em produção, vale a pena atualizar para as versões mais recentes já pelo ganho de latência no GC, mesmo sem mudar nenhuma linha do seu código.