Go Goroutines e Channels: Guia Prático de Concorrência
Você provavelmente já escreveu código Go que funciona bem até que deixa de funcionar — talvez uma data race derrube seu serviço sob carga, ou um deadlock congele sua API. A concorrência é um dos maiores atrativos do Go, mas também é onde muitos desenvolvedores ficam travados. Goroutines e channels são os blocos de construção, mas usá-los corretamente exige entender seu comportamento e as armadilhas comuns.
Este guia apresenta padrões práticos para usar goroutines e channels, com exemplos de código e conselhos que você pode aplicar imediatamente.
O que são Goroutines?
Uma goroutine é uma thread leve gerenciada pelo runtime do Go. Você cria uma ao prefixar uma chamada de função com go:
go doSomething()
Goroutines começam com uma pilha pequena (alguns kilobytes) que cresce conforme necessário. Isso torna barato criar milhares ou até milhões delas. O escalonador do Go multiplexa goroutines em threads do sistema operacional, então você não gerencia threads diretamente.
Mas criar uma goroutine sem coordenação é uma receita para o caos. Você precisa de uma forma de esperar por elas, passar dados ou cancelá-las — é aí que entram os channels.
Channels: Comunicação Entre Goroutines
Channels são condutores tipados para enviar e receber valores. Eles fornecem sincronização: um envio bloqueia até que um receptor esteja pronto, e um recebimento bloqueia até que um valor esteja disponível. Isso torna os channels ideais para coordenar goroutines.
Crie um channel com make(chan T):
ch := make(chan int)
go func() {
ch <- 42 // send
}()
value := <-ch // receive
Por padrão, os channels são sem buffer, o que significa que envios e recebimentos acontecem em compasso. Channels com buffer permitem um número limitado de envios sem um receptor:
ch := make(chan int, 3) // buffer size 3
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 would block until someone receives
Channels com buffer podem suavizar picos, mas não eliminam a necessidade de coordenação.
Padrões Comuns de Concorrência
1. Pool de Workers
Distribua tarefas entre um número fixo de workers para limitar o uso de recursos:
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for i := 1; i <= 5; i++ {
jobs <- i
}
close(jobs)
for r := 1; r <= 5; r++ {
<-results
}
}
Este padrão é ótimo para tarefas limitadas por CPU ou IO onde você quer controlar o paralelismo.
2. Fan-Out, Fan-In
Fan-out significa que várias goroutines leem do mesmo channel. Fan-in significa mesclar resultados de vários channels em um só:
func fanIn(chs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
wg.Add(len(chs))
for _, ch := range chs {
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
Use isso quando você tem múltiplos produtores e quer um único fluxo de resultados.
3. Select para Multiplexação
A instrução select permite que uma goroutine aguarde múltiplas operações de channel:
select {
case msg := <-ch1:
fmt.Println("received from ch1:", msg)
case ch2 <- 42:
fmt.Println("sent to ch2")
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}
Use select com um caso default para operações não bloqueantes, ou com um timeout para evitar esperar para sempre.
Armadilhas e Como Evitá-las
- Vazamentos de goroutine: Uma goroutine bloqueada para sempre em um envio/recebimento de channel nunca será coletada pelo garbage collector. Sempre garanta que os channels sejam fechados ou use cancelamento por contexto.
- Deadlocks: Dependências circulares entre channels causam deadlocks. Use timeouts ou projete sem ciclos.
- Data races: Acessar memória compartilhada sem sincronização leva a races. Use channels ou mutexes para proteger o estado compartilhado.
- Fechar channels: Apenas o remetente deve fechar um channel. Fechar pelo receptor pode causar panics. Além disso, enviar em um channel fechado causa panic.
Boas Práticas
- Prefira channels para coordenação, mutexes para estado: Use channels para passar dados e sinalizar entre goroutines; use
sync.Mutexpara proteger estado compartilhado simples. - Use context para cancelamento: Propague
context.Contextpara permitir desligamento gracioso e timeouts. - Limite a concorrência: Use pools de workers ou semáforos para evitar sobrecarregar recursos.
- Teste com o detector de race: Execute
go test -racepara detectar data races cedo. - Mantenha simples: Não complique demais. Frequentemente um mutex é mais simples e rápido do que uma coreografia complexa de channels.
Comparação: Channels vs. Mutexes
| Aspecto | Channels | Mutexes |
|---|---|---|
| Caso de uso | Passar dados, coordenação | Proteger estado compartilhado |
| Complexidade | Maior para tarefas simples | Menor para tarefas simples |
| Desempenho | Overhead de sincronização | Geralmente mais rápido para seções críticas curtas |
| Risco | Deadlocks, vazamentos | Deadlocks se não tomar cuidado |
FAQ
Quando devo usar um channel com buffer?
Use um channel com buffer quando quiser desacoplar remetentes e receptores para lidar com picos, ou quando souber o número exato de envios e quiser evitar bloqueios. No entanto, channels sem buffer fornecem garantias de sincronização mais fortes e geralmente são mais fáceis de raciocinar.
Como evito vazamentos de goroutine?
Garanta que toda goroutine tenha um caminho de saída claro. Use context.Context para sinalizar cancelamento, feche channels quando não houver mais valores a enviar e evite bloquear indefinidamente em operações de channel sem um timeout ou caso default.
Posso usar channels para tudo?
Não. Embora channels sejam poderosos, eles adicionam overhead e complexidade. Para estado compartilhado simples, um mutex geralmente é mais direto e eficiente. O provérbio Go diz: "Não se comunique compartilhando memória; compartilhe memória se comunicando", mas isso não significa que channels são sempre a resposta.
Se você está depurando problemas de concorrência ou analisando logs dos seus serviços Go, o Nginx Log Analyzer pode ajudar você a identificar padrões e erros rapidamente.