TL;DR: uma proposta aceita para o Go 1.27 adiciona um novo perfil de runtime/pprof, chamado goroutineleak, para detectar goroutines que ficaram bloqueadas para sempre. A ideia é usar uma rodada especial do garbage collector para separar goroutines que ainda podem voltar a executar daquelas presas em objetos de sincronização inalcançáveis. Isso não substitui boas práticas com context, timeouts e fechamento correto de channels, mas pode virar uma ferramenta importante para investigar leaks em serviços Go de longa duração.
O que foi aceito
A proposta “Goroutine leak detection via garbage collection”, ligada à issue golang/go#74609, foi marcada como aceita e associada ao milestone Go 1.27. O objetivo é expor um novo profile chamado goroutineleak no ecossistema de profiling do Go.
Na prática, ele entraria na mesma família mental de perfis que desenvolvedores já usam com runtime/pprof e net/http/pprof: CPU, heap, goroutine, block e mutex. A diferença é que o perfil novo não mostraria apenas “quais goroutines existem agora”. Ele tentaria responder uma pergunta mais difícil: quais goroutines estão bloqueadas de um jeito que não tem mais caminho realista para desbloqueio?
Esse tipo de bug aparece muito em serviços que rodam por semanas: um worker esperando em um channel que ninguém mais escreve, uma goroutine presa em um receive depois que o produtor morreu, ou uma rotina de background criada sem uma estratégia clara de cancelamento. O contador de goroutines sobe, o heap cresce, o GC trabalha mais, mas o stack dump tradicional nem sempre deixa claro o que é vazamento e o que é espera legítima.
Como o GC entra na história
A sacada do design é tratar a detecção como um problema de alcançabilidade. Em vez de começar a marcação do GC por todas as goroutines, a rodada especial começa pelas goroutines que podem executar. A partir delas, o runtime marca os objetos alcançáveis no heap.
Depois disso, o runtime procura goroutines bloqueadas em primitivas de concorrência que foram marcadas como alcançáveis. Se uma goroutine bloqueada está esperando em algo que uma goroutine viva ainda pode alcançar, ela pode voltar a executar; então ela também passa a ser tratada como raiz de marcação. O processo se repete até chegar a um ponto fixo.
O que sobra fora desse conjunto são goroutines candidatas a leak: elas estão bloqueadas em estruturas que não são alcançáveis a partir do programa que continua rodando. Antes de terminar, a proposta preserva a correção normal do GC fazendo uma marcação final que inclui essas goroutines também, para que o coletor não libere memória que ainda é referenciada por seus stacks.
Um exemplo simplificado do tipo de situação que a ferramenta quer tornar mais visível:
func startWorker() {
ch := make(chan struct{})
go func() {
<-ch
cleanup()
}()
}
Quando startWorker retorna, ninguém mais mantém uma referência a ch. A goroutine continua bloqueada no receive, mas não há mais produtor possível. Em um stack dump comum, ela aparece como “goroutine waiting on chan receive”; o valor novo estaria em destacar que essa espera é inalcançável a partir do restante do programa.
Como o perfil seria usado
O design propõe que solicitar o profile goroutineleak dispare uma rodada de GC voltada para detecção de leaks, colete o perfil de goroutines e filtre o resultado. Com debug < 2, a saída deve focar nas goroutines consideradas vazadas. Com debug >= 2, a saída vira um dump completo de goroutines.
Se a API final seguir o padrão atual de runtime/pprof, o uso programático ficaria parecido com outros perfis:
p := pprof.Lookup("goroutineleak")
if p != nil {
p.WriteTo(os.Stdout, 1)
}
E, em aplicações que expõem net/http/pprof, é razoável esperar uma experiência próxima de consultar um endpoint de profile. Ainda assim, vale cautela: a proposta fala em um mecanismo experimental, e a issue pública menciona GOEXPERIMENT=goroutineleakprofile. Nome de experimento, formato de saída e detalhes operacionais podem mudar antes do Go 1.27 final.
Por que isso importa em produção
Leak de goroutine é um problema particularmente traiçoeiro em Go porque goroutines são baratas. Isso é uma virtude da linguagem, mas também atrasa o sintoma: você pode criar milhares de goroutines presas antes de ver CPU, memória ou latência gritarem.
Hoje, a investigação costuma combinar métricas como go_goroutines, alertas de crescimento contínuo, dumps em /debug/pprof/goroutine e inspeção manual dos stacks. Funciona, mas exige experiência para separar uma goroutine “normalmente bloqueada” de uma goroutine que nunca mais será acordada.
O novo perfil promete reduzir essa ambiguidade. Ele não olha só para o estado local da goroutine; ele usa o grafo de objetos do heap para inferir se a primitiva que a bloqueia ainda está conectada ao restante do programa vivo. Para times que operam APIs, consumers, pipelines e workers em Go, isso pode encurtar bastante o caminho entre “temos goroutines crescendo” e “este é o padrão de código que vazou”.
Limites importantes
A proposta afirma que a abordagem é teoricamente sólida e busca evitar falsos positivos, mas não é mágica. O próprio design aponta que a qualidade da detecção piora quando recursos do heap são expostos de forma ampla ou ficam todos alcançáveis entre si. Em sistemas com registries globais, caches compartilhados ou estruturas muito conectadas, pode ser mais difícil provar que uma goroutine não voltará a executar.
Também há complexidade interna relevante. O documento descreve mudanças delicadas no runtime, incluindo tratamento especial para ponteiros em estruturas como sudog, usadas para representar goroutines esperando em channels e semáforos. Isso explica por que a funcionalidade está sendo tratada como experimento antes de virar ferramenta comum.
O ponto prático: continue escrevendo goroutines com ciclo de vida claro. Use context.Context, feche channels no dono correto, propague cancelamento e teste caminhos de shutdown. O goroutineleak deve ajudar a diagnosticar quando algo escapa, não justificar concorrência sem dono.