Tecnologias/01 - Linguagens/01 - Go5 min
Goroutines e Channels
Conceito
O problema que goroutine resolve é custo. Uma thread do sistema operacional custa 1–8 MB de stack reservado e um agendamento no kernel; dez mil threads são um problema de capacidade. Uma goroutine começa com ~2 KB de stack, cresce e encolhe sob demanda, e é agendada em user space. Um milhão de goroutines cabe num processo comum.
O truque é o scheduler M:N do runtime (modelo G-M-P):
G (goroutine) → milhares, baratas
M (thread OS) → poucas, uma por core ocupado
P (processor) → GOMAXPROCS, a permissão de executar
Cada P tem sua fila local de goroutines e rouba trabalho (work stealing) da fila
de outro quando fica vazia. Desde o Go 1.14 a preempção é assíncrona: uma goroutine em
laço apertado não trava mais o scheduler.
O detalhe que muda tudo: I/O não bloqueia thread
Quando você escreve conn.Read(buf), o código parece bloqueante e é fácil de ler.
Por baixo, o runtime registra o descritor no netpoller (epoll no Linux, kqueue no
BSD), estaciona a goroutine e libera a thread para outra goroutine.
Você escreve código sequencial e ganha o comportamento de um event loop — sem
async/await, sem callback hell, sem colorir funções.
É essa combinação (goroutine barata + netpoller transparente) que faz um serviço Go segurar dezenas de milhares de conexões simultâneas com meia dúzia de threads. Compare com modelo thread-per-request clássico.
Channels e CSP
Channel é uma fila tipada com sincronização embutida. O lema da comunidade:
Não comunique compartilhando memória; compartilhe memória comunicando.
ch := make(chan int) // sem buffer: send espera receive (rendez-vous)
ch := make(chan int, 100) // com buffer: send só bloqueia se estiver cheio
select espera em vários channels ao mesmo tempo — é o switch da concorrência:
select {
case v := <-work:
processa(v)
case <-ctx.Done(): // cancelamento: sempre tenha este caso
return ctx.Err()
case <-time.After(2 * time.Second):
return errTimeout
}
E context.Context é o mecanismo padrão de cancelamento em cascata: quem chama
cancela, e todo mundo abaixo na árvore vê ctx.Done() fechar. É a razão de quase toda
função de biblioteca em Go começar com ctx context.Context.
Trade-offs
Channel não é a ferramenta padrão. A cultura Go inicial exagerou. Para proteger um
contador ou um mapa, sync.Mutex é mais simples e mais rápido; channel serve para
transferir posse de um dado ou coordenar etapas. Rob Pike já disse o essencial: use
o que deixar o código mais claro.
Channels não eliminam data race. Nada impede você de mandar um *User por um
channel e continuar escrevendo nele do outro lado. O error que o compilador não pega, o
go test -race pega — e ele deveria estar no seu CI.
Concorrência barata convida a concorrência ilimitada. Este é o erro mais comum e o mais caro:
for _, id := range ids { // 500 mil ids
go buscar(id) // 500 mil chamadas simultâneas ao banco
}
Não é "escalável", é uma negação de serviço contra a sua própria infraestrutura — e
contra o pool de conexões do banco (5. Go em Produção). Concorrência precisa de
limite explícito: errgroup.SetLimit, semáforo com channel, ou worker pool. É o mesmo
raciocínio de Back Pressure e Queue-Based Load Leveling.
O trade-off central: o Go tornou concorrência tão barata que a dificuldade migrou de criar trabalho paralelo para limitar e encerrar trabalho paralelo. Toda goroutine que você inicia é uma pergunta em aberto: quem garante que ela termina?
Exemplo prático
Worker pool com limite, cancelamento e propagação de erro — o padrão que resolve 90% dos casos reais:
import "golang.org/x/sync/errgroup"
func processarTodos(ctx context.Context, ids []int) error {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(16) // no máximo 16 em voo — protege o banco e a memória
for _, id := range ids {
id := id // desnecessário desde o Go 1.22 (loop var por iteração)
g.Go(func() error {
select {
case <-ctx.Done(): // o primeiro erro cancela o resto
return ctx.Err()
default:
}
return processar(ctx, id)
})
}
return g.Wait() // devolve o primeiro erro, esperando todas terminarem
}
sync.WaitGroup.GoDesde o Go 1.25 o próprio sync.WaitGroup tem o método Go, que dispara a goroutine e
cuida do Add/Done. Para o caso simples (sem erro, sem limite) já não é preciso
errgroup.
Como isso falha em produção
| Falha | Causa | Sintoma |
|---|---|---|
| Goroutine leak | goroutine esperando em channel que ninguém mais vai usar | memória e contagem de goroutines só sobem |
| Deadlock | send sem receive, ou dois locks em ordens diferentes | fatal error: all goroutines are asleep |
| OOM | goroutines sem limite × buffer por goroutine | OOMKilled no contêiner |
| Race | struct compartilhada sem lock | resultado errado só sob carga |
time.After em loop |
cada chamada aloca um timer que só é liberado no fim | crescimento lento de memória; use time.NewTimer + Stop |
O vazamento de goroutine é o mais insidioso, porque o programa continua correto —
só não devolve recursos. E era difícil de diagnosticar: pprof mostrava 40 mil
goroutines e cabia a você adivinhar quais eram legítimas.
goroutineleakO Go 1.27 promoveu a detector de vazamento a perfil normal: runtime/pprof expõe
goroutineleak (e net/http/pprof publica /debug/pprof/goroutineleak), que roda o
GC e lista as goroutines permanentemente bloqueadas em channel, mutex ou similar —
inalcançáveis por qualquer goroutine executável. Deixou de ser adivinhação. Ver
4. Versões, Módulos e Compatibilidade.
Aprofundamento aplicado
Cancelamento precisa atravessar todos os bloqueios
Considere um produtor que envia resultados para um consumidor que pode desistir.
O envio também precisa observar o cancelamento, usando context:
select {
case resultados <- valor:
// O valor foi entregue ou entrou no buffer.
case <-ctx.Done():
return ctx.Err()
}
Esse trecho pressupõe ctx, resultados e valor definidos pela rotina chamadora.
Se ninguém consome e o canal está cheio, o segundo caso oferece um caminho de saída.
A mesma análise vale para receber trabalho e chamar dependências externas.
O cancelamento é cooperativo: cancel() sinaliza, mas não mata a goroutine. Quem cria
as rotinas precisa esperar seu término, e quem fecha o canal precisa saber que nenhum
produtor continuará enviando. Um canal de capacidade 100 adia o bloqueio; não resolve
um consumidor permanentemente mais lento.
Referência: contexto e cancelamento.
Exercício: faça o consumidor parar após o primeiro resultado. Resultado esperado: todos os produtores encerram, inclusive os que estavam esperando enviar; a operação não deixa goroutines acumulando a cada execução.
Relacionado
- 1. Go
- 5. Go em Produção
- 3. Erros, Interfaces e o Estilo Go
- Back Pressure
- Competing Consumers
- 6. Pilha de Chamadas
Parte de MOC Go · go.dev/doc/effective_go