Channels: comunicando goroutines com segurança
1. O Problema da Comunicação entre Goroutines
Quando trabalhamos com concorrência em Go, um dos maiores desafios é coordenar o acesso a dados compartilhados entre múltiplas goroutines. Sem uma estratégia adequada, somos rapidamente confrontados com condições de corrida (race conditions), onde o resultado da execução depende da ordem imprevisível de acesso concorrente.
Tradicionalmente, linguagens de programação resolvem esse problema com locks, mutexes e memória compartilhada. Go, porém, adota uma filosofia diferente. No famoso slogan do Go:
"Não se comunique compartilhando memória; compartilhe memória se comunicando."
Isso significa que, em vez de proteger regiões críticas com locks, devemos passar dados entre goroutines através de canais de comunicação — os channels. Essa abordagem elimina uma vasta gama de bugs relacionados à concorrência, pois cada dado pertence a apenas uma goroutine por vez.
2. Fundamentos de Channels em Go
Um channel em Go é um tubo tipado através do qual você pode enviar e receber valores. Sua sintaxe é simples:
// Channel unbuffered (sem buffer)
ch := make(chan int)
// Channel buffered (com buffer de capacidade 3)
chBuf := make(chan string, 3)
O operador <- é usado tanto para enviar quanto para receber:
ch <- 42 // Enviar 42 para o channel
valor := <-ch // Receber um valor do channel
A diferença fundamental entre unbuffered e buffered está no comportamento de sincronização.
3. Channels Unbuffered: Sincronização Pura
Um channel unbuffered não possui capacidade de armazenamento. Isso significa que cada envio bloqueia até que haja uma goroutine receptora pronta, e vice-versa. Esse comportamento cria um handshake entre as goroutines.
Veja um exemplo clássico de produtor-consumidor:
func produtor(ch chan<- int) {
for i := 1; i <= 5; i++ {
fmt.Printf("Produzindo: %d\n", i)
ch <- i // Bloqueia até o consumidor receber
}
close(ch)
}
func consumidor(ch <-chan int) {
for valor := range ch {
fmt.Printf("Consumindo: %d\n", valor)
time.Sleep(100 * time.Millisecond) // Simula trabalho
}
}
func main() {
ch := make(chan int)
go produtor(ch)
consumidor(ch)
}
Aqui, cada item só é produzido quando o anterior é consumido. Isso garante sincronização sem locks explícitos.
4. Channels com Buffer: Tolerância a Atrasos
Channels buffered possuem uma capacidade definida. Enquanto o buffer não estiver cheio, o envio não bloqueia. Isso permite desacoplamento temporal entre produtor e consumidor.
func main() {
ch := make(chan string, 3)
// Podemos enviar até 3 itens sem receptor
ch <- "msg1"
ch <- "msg2"
ch <- "msg3"
fmt.Println(<-ch) // msg1
fmt.Println(<-ch) // msg2
fmt.Println(<-ch) // msg3
}
Cuidado: buffer cheio causa bloqueio no envio; buffer vazio causa bloqueio na recepção. Deadlocks são comuns quando a capacidade é mal dimensionada.
5. Padrões de Fechamento e Range
A função close() sinaliza que nenhum valor será enviado. Isso permite que receptores usem for range para consumir até o fim:
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 3; i++ {
ch <- i
}
close(ch) // Sinaliza fim dos dados
}()
for valor := range ch {
fmt.Println(valor) // 0, 1, 2
}
}
Para verificar se um channel foi fechado:
valor, ok := <-ch
if !ok {
fmt.Println("Channel fechado")
}
6. Select: Multiplexando Múltiplos Channels
O select permite que uma goroutine aguarde múltiplas operações de channel simultaneamente:
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "resposta lenta"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "resposta mais lenta"
}()
select {
case msg1 := <-ch1:
fmt.Println("Recebido de ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("Recebido de ch2:", msg2)
case <-time.After(500 * time.Millisecond):
fmt.Println("Timeout! Nenhuma resposta em 500ms")
}
}
Padrões úteis com select:
- Timeout: case <-time.After(duration)
- Cancelamento: combinado com context.Context
- Operação não-bloqueante: default: case
7. Padrões Avançados com Channels
Fan-in: Múltiplas fontes, um destino
func fanIn(ch1, ch2 <-chan string) <-chan string {
ch := make(chan string)
go func() {
for {
select {
case v := <-ch1:
ch <- v
case v := <-ch2:
ch <- v
}
}
}()
return ch
}
Fan-out: Um produtor, múltiplos workers
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
results <- job * 2 // Processa o job
}
}
func main() {
jobs := make(chan int, 10)
results := make(chan int, 10)
// Inicia 3 workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Envia 5 jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// Coleta resultados
for r := 1; r <= 5; r++ {
<-results
}
}
Pipeline: Encadeamento de stages
func main() {
// Stage 1: gera números
numeros := make(chan int)
go func() {
for i := 1; i <= 5; i++ {
numeros <- i
}
close(numeros)
}()
// Stage 2: dobra os números
dobrados := make(chan int)
go func() {
for n := range numeros {
dobrados <- n * 2
}
close(dobrados)
}()
// Stage 3: consome
for d := range dobrados {
fmt.Println(d)
}
}
8. Boas Práticas e Armadilhas Comuns
❌ Nunca enviar em channel fechado
ch := make(chan int)
close(ch)
ch <- 42 // PANIC: send on closed channel
❌ Goroutine leaks
Sempre garanta que goroutines que enviam para channels sejam finalizadas. Use close() ou cancele com context:
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // Garante que a goroutine termine
✅ Preferir channels tipados
Evite chan interface{}. Channels tipados trazem segurança de tipo e melhor legibilidade:
// Ruim
ch := make(chan interface{})
ch <- "texto"
valor := <-ch // Precisa de type assertion
// Bom
ch := make(chan string)
ch <- "texto"
valor := <-ch // Tipo garantido
✅ Documentar a direção do channel
Use chan<- (send-only) e <-chan (receive-only) em parâmetros de função para deixar explícita a intenção.
Referências
- Effective Go: Channels — Documentação oficial sobre o uso idiomático de channels em Go
- Go by Example: Channels — Tutoriais práticos com exemplos de código sobre channels
- The Go Memory Model — Especificação oficial do modelo de memória e garantias de sincronização com channels
- Concurrency in Go: Tools and Techniques — Livro de Katherine Cox-Buday com padrões avançados de concorrência
- Go Concurrency Patterns: Pipelines and cancellation — Artigo oficial do blog Go sobre pipelines com channels e cancelamento
- Understanding Go Channels: An In-Depth Guide — Guia detalhado sobre funcionamento interno e padrões de channels