Programação concorrente com channels em Go
1. Fundamentos da Concorrência em Go
Go foi projetado desde sua origem para lidar com concorrência de forma nativa e elegante. O coração desse modelo são as goroutines — funções ou métodos que executam concorrentemente com outras goroutines, dentro do mesmo espaço de endereçamento. Diferente de threads de sistema operacional, goroutines são extremamente leves (apenas alguns KB de pilha) e gerenciadas pelo runtime do Go.
func dizerOla() {
fmt.Println("Olá, mundo concorrente!")
}
func main() {
go dizerOla() // executa em uma nova goroutine
time.Sleep(time.Second) // espera para ver o resultado
}
Enquanto linguagens tradicionais usam locks, mutexes e variáveis de condição para sincronização, Go propõe um paradigma diferente: "não se comunique compartilhando memória; compartilhe memória se comunicando". Essa filosofia vem do modelo CSP (Communicating Sequential Processes), formalizado por Tony Hoare em 1978, e que Go implementa através de channels.
2. Channels: Tipos, Criação e Operações Essenciais
Channels são os tubos que conectam goroutines concorrentes. Você pode enviar valores de um lado e recebê-los do outro. Em Go, channels são tipados — cada channel só transporta um tipo específico de dado.
// Channel unbuffered (sem buffer)
ch := make(chan int)
// Channel buffered (com buffer de 3 posições)
chBuf := make(chan string, 3)
// Enviar dados
ch <- 42
// Receber dados
valor := <-ch
Channels unbuffered bloqueiam o envio até que outra goroutine esteja pronta para receber — isso cria um ponto de sincronização natural. Channels buffered permitem enviar até que o buffer esteja cheio, sem bloquear imediatamente.
Fechar um channel com close(ch) é importante para sinalizar que não haverá mais envios. O receptor pode detectar isso:
valor, ok := <-ch
if !ok {
// channel foi fechado
}
3. Padrões de Comunicação com Channels
Fan-in
Múltiplas goroutines produtoras enviam dados para um único channel consumidor:
func produtor(id int, ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- id*100 + i
}
}
func main() {
ch := make(chan int, 10)
go produtor(1, ch)
go produtor(2, ch)
for i := 0; i < 10; i++ {
fmt.Println(<-ch)
}
}
Fan-out
Uma goroutine distribui trabalho para múltiplos workers concorrentes:
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
results <- job * 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 j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
}
Pipeline
Encadeamento de stages onde a saída de um é a entrada do próximo:
func gerar(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func quadrado(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
4. Sincronização e Controle de Fluxo
Channels podem atuar como semáforos para limitar concorrência:
func main() {
sem := make(chan struct{}, 3) // permite até 3 goroutines simultâneas
for i := 0; i < 10; i++ {
go func(id int) {
sem <- struct{}{} // adquire slot
defer func() { <-sem }() // libera slot
fmt.Printf("Trabalhando %d\n", id)
time.Sleep(time.Second)
}(i)
}
}
Para timeouts, combinamos select com time.After:
ch := make(chan int)
select {
case resultado := <-ch:
fmt.Println("Resultado:", resultado)
case <-time.After(2 * time.Second):
fmt.Println("Timeout!")
}
5. Select: Gerenciando Múltiplos Channels
A estrutura select permite que uma goroutine aguarde múltiplas operações de channel simultaneamente:
select {
case msg1 := <-ch1:
fmt.Println("Recebido de ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("Recebido de ch2:", msg2)
case ch3 <- 42:
fmt.Println("Enviado 42 para ch3")
default:
fmt.Println("Nenhum canal pronto")
}
O default torna o select non-blocking — se nenhum canal estiver pronto, executa o caso default imediatamente. Isso é útil para implementar polling ou evitar deadlocks.
6. Boas Práticas e Armadilhas Comuns
Deadlocks ocorrem frequentemente com channels unbuffered quando não há correspondência entre envios e recebimentos:
// Deadlock! Envio bloqueia, mas ninguém recebe
ch := make(chan int)
ch <- 42 // deadlock
Vazamento de goroutines acontece quando uma goroutine fica esperando eternamente em um channel que nunca será fechado ou receberá dados. O padrão "done channel" resolve isso:
func main() {
done := make(chan struct{})
go func() {
// trabalho...
close(done) // sinaliza conclusão
}()
<-done // aguarda finalização
}
Sempre feche channels do lado do produtor, nunca do consumidor. E evite fechar channels que já estão sendo usados por múltiplos produtores — nesse caso, use um channel de "done" separado.
7. Casos de Uso Avançados
Workers Pool com Channels
func workerPool(tasks []int, numWorkers int) []int {
taskCh := make(chan int, len(tasks))
resultCh := make(chan int, len(tasks))
// Inicia workers
for w := 0; w < numWorkers; w++ {
go func() {
for task := range taskCh {
resultCh <- task * task
}
}()
}
// Envia tarefas
for _, task := range tasks {
taskCh <- task
}
close(taskCh)
// Coleta resultados
var results []int
for i := 0; i < len(tasks); i++ {
results = append(results, <-resultCh)
}
return results
}
Rate Limiting com Channel Ticker
func rateLimitedProcess(requests []int) {
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
for _, req := range requests {
<-ticker.C // aguarda o tick
go func(r int) {
fmt.Println("Processando:", r)
}(req)
}
}
Orquestração com Context e Channels
func processWithTimeout(ctx context.Context, ch chan int) {
select {
case val := <-ch:
fmt.Println("Valor recebido:", val)
case <-ctx.Done():
fmt.Println("Cancelado:", ctx.Err())
}
}
Conclusão
Channels em Go oferecem um modelo de concorrência poderoso e seguro, baseado em comunicação em vez de memória compartilhada. Dominar padrões como fan-in, fan-out, pipelines e o uso correto de select permite construir sistemas concorrentes robustos, evitando deadlocks e vazamentos de goroutines. A simplicidade do modelo CSP em Go torna a concorrência acessível sem sacrificar desempenho ou segurança.
Referências
- Effective Go: Concurrency — Documentação oficial com padrões e boas práticas de concorrência em Go
- Go by Example: Channels — Tutoriais práticos com exemplos interativos de channels em Go
- The Go Blog: Share Memory By Communicating — Artigo oficial explicando a filosofia de comunicação via channels
- Go Concurrency Patterns: Pipelines and cancellation — Padrões avançados de pipeline com cancelamento usando channels
- Understanding Go's
selectstatement — Guia detalhado sobre multiplexação de channels com select - Go Wiki: Learn Concurrency — Curadoria de recursos para aprendizado de concorrência em Go