Depuração de Vazamentos de Goroutines em Go: 5 Padrões de Concorrência Estruturada para Eliminar Vazamentos 2026
Você Já Encontrou Este Cenário Misterioso?
Seu serviço de produção funciona perfeitamente por algumas horas, então o uso de memória continua aumentando, a CPU atinge 90 %, e quando você verifica o pprof — milhares de goroutines estão presas em recepções de canais, esperando dados que nunca chegam. Após uma reinicialização, tudo volta ao normal, mas o problema reaparece horas depois. Este é o clássico vazamento de goroutine, o bug mais insidioso e mortal na programação concorrente em Go.
Pior ainda, vazamentos de goroutines não lançam erros diretamente. Eles consomem lentamente os recursos do sistema como um veneno crônico até que o OOM Killer intervenha. Estamos em 2026 — não deveríamos mais escrever código concorrente com goroutines "disparar e esquecer". Concorrência Estruturada é a solução.
O que é Concorrência Estruturada?
Concorrência estruturada é um paradigma de programação que vincula o ciclo de vida das goroutines ao escopo léxico de seu criador. Princípio central: se uma função cria uma goroutine, essa goroutine deve terminar antes do retorno da função.
| Conceito | Abordagem Tradicional | Concorrência Estruturada |
|---|---|---|
| Ciclo de vida da goroutine | Não gerenciado, depende do desenvolvedor | Vinculado automaticamente ao escopo |
| Tratamento de erros | log.Println manual ou ignorado |
Propagação automática de erros via errgroup |
| Propagação de cancelamento | Gerenciamento manual de Context | Automática, cancelamento pai cancela tudo |
| Liberação de recursos | defer em goroutines não limpas |
Limpeza estruturada garantida |
| Testabilidade | Difícil de testar, condições de corrida difíceis de reproduzir | Fácil de testar, caminho de execução determinístico |
5 Principais Pontos Dolorosos de Vazamentos de Goroutines
Ponto Doloroso 1: A Goroutine Nunca Termina
func watch() {
for {
val := <-someCh // Se someCh nunca for fechado, fica preso para sempre
process(val)
}
}
A causa mais comum de vazamentos: goroutines esperando operações de canal que nunca são sinalizadas. Quando a fonte é fechada ou cancelada, a goroutine deve ser notificada.
Ponto Doloroso 2: Erros São Ignorados
go func() {
if err := doWork(); err != nil {
log.Printf("Erro: %v", err) // Apenas registrado, nunca reportado
}
}()
O chamador nunca sabe se a goroutine teve sucesso. Quando várias goroutines são executadas simultaneamente e uma falha, as outras devem ser informadas para parar — mas não há mecanismo para isso.
Ponto Doloroso 3: Cancelamento Não é Propagado
Quando o processo principal é cancelado, as goroutines filhas também devem parar. O código tradicional frequentemente não tem nenhum mecanismo de cancelamento, então mesmo após o retorno da função principal, as goroutines em segundo plano continuam consumindo CPU e memória.
Ponto Doloroso 4: Sem Timeout de Recursos
resp, err := http.Post(url, "application/json", body)
// E se o serviço não responder? Esta goroutine bloqueia para sempre
Operações de rede, operações de arquivo, consultas de banco de dados — qualquer operação de E/S sem timeout pode se tornar um vazamento de goroutine.
Ponto Doloroso 5: Pânico Causa Queda de Toda a Aplicação
go func() {
doWork() // Se doWork entrar em pânico, toda a aplicação cai
}()
Pânicos em goroutines sem recovery fazem todo o processo travar. O código de produção deve capturar pânicos em cada goroutine.
5 Padrões de Concorrência Estruturada
Padrão 1: errgroup — Propagação Automática de Cancelamento
errgroup.Group combina gerenciamento de ciclo de vida de goroutines, propagação de erros e cancelamento em um só.
package main
import (
"context"
"fmt"
"net/http"
"time"
"golang.org/x/sync/errgroup"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
g, ctx := errgroup.WithContext(ctx)
urls := []string{
"https://api.example.com/users",
"https://api.example.com/orders",
"https://api.example.com/products",
}
results := make([]string, len(urls))
for i, url := range urls {
i, url := i, url
g.Go(func() error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("criar requisição %s: %w", url, err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("obter %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("obter %s: status %d", url, resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("ler %s: %w", url, err)
}
results[i] = string(body)
return nil
})
}
if err := g.Wait(); err != nil {
fmt.Printf("Erro em obtenções concorrentes: %v\n", err)
return
}
for i, url := range urls {
fmt.Printf("%s: %d bytes\n", url, len(results[i]))
}
}
Pontos-chave de design do errgroup:
errgroup.WithContextcria um contexto de cancelamento com propagação automática- Qualquer goroutine que retorne um erro cancela automaticamente outras goroutines
g.Wait()aguarda todas as goroutines e retorna o primeiro erro- Deve ser combinado com
context.WithTimeoutousignal.NotifyContext
Padrão 2: Worker Pool — Concorrência Limitada Evita Esgotamento de Recursos
package workerpool
import (
"context"
"sync"
)
type Task func(ctx context.Context) error
type Pool struct {
workers int
tasks chan Task
wg sync.WaitGroup
}
func NewPool(workers int, bufferSize int) *Pool {
return &Pool{
workers: workers,
tasks: make(chan Task, bufferSize),
}
}
func (p *Pool) Start(ctx context.Context) {
for i := 0; i < p.workers; i++ {
p.wg.Add(1)
go func(id int) {
defer p.wg.Done()
for {
select {
case <-ctx.Done():
return
case task, ok := <-p.tasks:
if !ok {
return
}
_ = task(ctx)
}
}
}(i)
}
}
func (p *Pool) Submit(task Task) bool {
select {
case p.tasks <- task:
return true
default:
return false
}
}
func (p *Pool) Stop() {
close(p.tasks)
p.wg.Wait()
}
Pontos-chave de design do Worker Pool:
- Número fixo de workers evita explosão de goroutines
- Canal de tarefas com buffer serve como fila de tarefas
- Suporte a Context para cancelamento — workers podem sair limpos
- Submit não bloqueante, retorna false quando a fila está cheia
Padrão 3: Fan-Out/Fan-In — Dispersão e Coleta Paralelas
Fan-Out distribui fontes de dados para múltiplas goroutines para processamento paralelo, Fan-In mescla resultados de múltiplas goroutines em um canal.
func FanOut[T any](ctx context.Context, source <-chan T, workers int) []<-chan T {
channels := make([]<-chan T, workers)
for i := 0; i < workers; i++ {
ch := make(chan T)
channels[i] = ch
go func() {
defer close(ch)
for {
select {
case <-ctx.Done():
return
case val, ok := <-source:
if !ok {
return
}
select {
case ch <- val:
case <-ctx.Done():
return
}
}
}
}()
}
return channels
}
func FanIn[T any](ctx context.Context, channels ...<-chan T) <-chan T {
out := make(chan T)
var wg sync.WaitGroup
wg.Add(len(channels))
for _, ch := range channels {
go func(c <-chan T) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case val, ok := <-c:
if !ok {
return
}
select {
case out <- val:
case <-ctx.Done():
return
}
}
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
Padrão 4: Pipeline — Processamento por Etapas
Um pipeline decompõe o processamento complexo em etapas, cada uma executando como uma goroutine, conectadas por canais.
type Stage[In any, Out any] func(ctx context.Context, in <-chan In) <-chan Out
func NewStage[In any, Out any](
process func(ctx context.Context, in In) (Out, error),
bufferSize int,
) Stage[In, Out] {
return func(ctx context.Context, in <-chan In) <-chan Out {
out := make(chan Out, bufferSize)
go func() {
defer close(out)
for {
select {
case <-ctx.Done():
return
case val, ok := <-in:
if !ok {
return
}
result, err := process(ctx, val)
if err != nil {
continue
}
select {
case out <- result:
case <-ctx.Done():
return
}
}
}
}()
return out
}
}
Padrão 5: Worker Pool Dinâmico — Escalonamento Automático Baseado em Carga
Ajusta o número de workers com base no comprimento da fila, evitando desperdício de recursos.
type DynamicPool struct {
minWorkers int64
maxWorkers int64
active atomic.Int64
tasks chan Task
wg sync.WaitGroup
adjustTick *time.Ticker
}
func NewDynamicPool(minW, maxW int) *DynamicPool {
return &DynamicPool{
minWorkers: int64(minW),
maxWorkers: int64(maxW),
tasks: make(chan Task, maxW*2),
adjustTick: time.NewTicker(5 * time.Second),
}
}
func (p *DynamicPool) adjustWorkers(ctx context.Context) {
current := p.active.Load()
queueLen := len(p.tasks)
if queueLen > int(current) && current < p.maxWorkers {
p.addWorker(ctx)
log.Printf("[DynamicPool] Workers aumentados: %d → %d (fila: %d)", current, p.active.Load(), queueLen)
} else if queueLen == 0 && current > p.minWorkers {
p.removeWorker()
}
}
Tabela de Solução de Problemas
| Sintoma | Causa Provável | Método de Investigação | Solução |
|---|---|---|---|
| Contagem de goroutines continua aumentando | Vazamento de goroutine, canal nunca fechado | Monitorar runtime.NumGoroutine(), perfil pprof de goroutines |
Adicionar cancelamento de Context, garantir defer close(ch) |
| Memória continua crescendo | Goroutines retêm referências a objetos, acúmulo em canais | Perfil pprof heap, verificar tamanho do buffer do canal | Limitar buffer do canal, liberar referências prontamente |
Erro deadlock! |
Todas as goroutines bloqueadas | Verificar todos os pares de leitura/escrita de canais | Adicionar cancelamento de Context ou default no select |
| Todas as requisições falham | Context pai cancelado, todas as goroutines paradas | Verificar cadeia de Context | Ajustar timeout adequadamente |
| Resultados faltando | Algumas goroutines terminam antes de escrever | Verificar lógica de término das goroutines | Usar WaitGroup ou errgroup para garantir completude |
Resumo
Os 5 padrões de concorrência estruturada do Go 2026 fornecem soluções completas, desde fundamentos até cenários altamente otimizados:
- errgroup: Propagação automática de erros + cancelamento, melhor para tarefas paralelas independentes
- Worker Pool: Paralelismo fixo evita esgotamento de recursos, melhor para filas de tarefas
- Fan-Out/Fan-In: Dispersão e coleta paralelas, melhor para pipelines de processamento de dados
- Pipeline: Processamento por etapas com contrapressão, melhor para processamento de dados em tempo real
- Pool Dinâmico: Escalonamento elástico economiza recursos, melhor para cenários de carga variável
Guia de decisão: errgroup + Worker Pool cobrem 80 % dos cenários. Adicione Fan-Out/Fan-In e Pipeline para processamento complexo de dados. Use Pool Dinâmico apenas quando a carga de entrada variar significativamente.
Ferramentas Recomendadas
- /pt-BR/encode/json — Formatador JSON, depuração de respostas JSON de serviços concorrentes
- /pt-BR/encode/hash — Calculadora de hash, verificação de consistência de dados
- /pt-BR/encode/base64 — Codificação Base64, manipulação de codificação de dados binários
Experimente estas ferramentas executadas localmente no navegador — nenhum cadastro necessário →