TL;DR: uma nova proposta de design no repositório golang/proposal descreve runtime.freegc, um mecanismo interno para o compilador e o runtime devolverem memória comprovadamente morta ao alocador antes de uma rodada normal do GC chegar nela. Não é uma API para aplicações chamarem manualmente. A promessa é reduzir taxa efetiva de alocação, ciclos de garbage collection e custo de CPU em padrões comuns, como crescimento repetido de slices, strings.Builder, mapas e decodificação com muita memória temporária.

O que a proposta muda

O documento design/74299-runtime-freegc.md parte de uma observação simples: o programa Go frequentemente deixa de usar uma área de memória bem antes de o coletor de lixo conseguir reaproveitá-la. Hoje, mesmo quando o compilador ou o runtime já conseguem entender que um backing array antigo não será mais usado, essa memória normalmente continua no heap até o GC avançar o suficiente.

A proposta adiciona uma primitiva interna, runtime.freegc, para marcar um objeto do heap como reutilizável quando for seguro provar que ele está logicamente morto. O ponto importante é o “provar”: o design não propõe um free manual no estilo C, nem transfere para o desenvolvedor a responsabilidade de liberar memória. A chamada seria emitida por código do compilador ou do runtime em situações em que não há aliases utilizáveis para o objeto.

Isso coloca a ideia no mesmo território de otimizações como escape analysis e stack allocation: o código Go continua idiomático, mas o compilador ganha mais uma forma de reduzir trabalho futuro do GC.

Onde isso pode aparecer no código do dia a dia

Um dos casos mais concretos é crescimento de slices. Quando um append precisa criar um backing array maior, o array antigo fica morto se o compilador provar que nenhum outro slice ainda aponta para ele. A proposta descreve um caminho interno chamado runtime.growsliceNoAlias, parecido com o growslice atual, mas capaz de liberar o backing array antigo depois da cópia.

O código do usuário não mudaria:

func collect(input []int) []int {
    var out []int
    for _, x := range input {
        out = append(out, transform(x))
    }
    return out
}

Se out for comprovadamente não-aliasado durante os crescimentos intermediários, o runtime pode reaproveitar os arrays antigos mais cedo. O slice final continua válido e pode escapar normalmente como retorno da função.

A proposta também fala de alocações com make([]T, size) em que o backing array é temporário, tem tamanho dinâmico e precisa ir para o heap. Para esses casos, o compilador poderia usar alocações “rastreadas”, com nomes internos como runtime.makeslicetracked64 e runtime.freegcTracked, para liberar vários objetos ao fim de um escopo sem manter ponteiros artificialmente vivos.

Por que isso importa para performance

A melhoria não é “pausar menos o GC” por decreto. Ela tenta reduzir a quantidade de memória que o GC precisa tratar como pressão real de alocação. Se objetos mortos voltam ao alocador antes, o programa pode fazer menos alocações novas, acionar menos ciclos de coleta e passar menos tempo com write barriers ativas.

O design cita experimentos interessantes. Em um teste manual com strings.Builder, ganhos apareceram principalmente quando o builder crescia várias vezes: casos com muitos writes ficaram substancialmente mais rápidos porque buffers intermediários puderam ser reaproveitados. O texto também menciona experimentos em reflect combinados com json/v2, com reduções relevantes de alocação em alguns benchmarks reais de unmarshal.

Esses números não devem ser lidos como promessa direta para qualquer serviço Go. Eles mostram onde a técnica tem potencial: código que cria estruturas temporárias grandes, cresce buffers repetidamente ou faz muito trabalho de serialização. Em APIs e workers que já são sensíveis a GC, reduzir a pressão de heap pode aparecer como menor CPU, menor latência de cauda ou simplesmente mais folga antes de precisar mexer em pooling manual.

O que não muda para aplicações

O mais saudável, por enquanto, é tratar runtime.freegc como detalhe de implementação. Desenvolvedores não devem planejar código chamando essa função. O próprio design descreve restrições fortes: ponteiros precisam ser para objetos do heap, tamanhos e metadados importam, objetos pequenos ou grandes demais podem não ser tratados, e finalizers ou cleanups criados pelo usuário ainda exigem cuidado especial.

Também há um detalhe contraintuitivo: inserir uma chamada tarde demais pode piorar liveness, porque passar o ponteiro para freegc mantém esse ponteiro vivo até a chamada. É justamente por isso que a proposta depende de integração fina com o compilador, e não de uma API pública.

Para quem escreve Go em produção, a recomendação prática continua a mesma: prefira código claro, meça com benchmarks e perfis, e evite pools manuais antes de ter evidência. Se essa proposta avançar até um release, parte do ganho esperado virá de código comum ficando mais eficiente sem alterações na base.

Como acompanhar

A proposta ainda é uma peça de design com implementação inicial e vários detalhes em evolução. O documento menciona uma pilha de CLs, trabalho de análise de aliases no compilador e limitações atuais, como casos de make dentro de loops não estruturados. Também há relação com outras frentes de runtime, como mapas liberando armazenamento morto durante crescimento e split, desde que não haja iteração retendo as tabelas antigas.

O sinal mais importante é a direção: o time de Go segue investindo em reduzir custo de memória sem exigir que o usuário abandone o modelo seguro da linguagem. Depois de melhorias recentes em diagnóstico, como o perfil proposto para leak de goroutines, runtime.freegc aponta para outra frente do mesmo tema: tornar o runtime mais capaz de entender o ciclo de vida real dos objetos.

Saiba mais