Channels avançado
Esta aula explora conceitos avançados de channels em Go, incluindo fechamento de channels, uso de range para iteração, nil channels e padrões comuns de concorrência. O conteúdo é prático, com exemplos de código e exercícios resolvidos.
Channels são uma das primitivas de concorrência mais poderosas em Go, permitindo comunicação segura entre goroutines. Nesta aula, vamos nos aprofundar em tópicos avançados como fechamento de channels, iteração com range, comportamento de nil channels e padrões de design que tornam o código concorrente mais robusto e eficiente.
Dominar esses conceitos é essencial para escrever programas concorrentes corretos e evitar armadilhas comuns, como deadlocks e vazamentos de goroutines.
Fechando channels
Em Go, um channel pode ser fechado usando a função close(ch). Fechar um channel sinaliza que nenhum valor será mais enviado por ele. Isso é útil para notificar receptores de que não há mais dados a serem lidos.
Após fechar um channel, qualquer tentativa de enviar um valor resultará em pânico. No entanto, é seguro continuar recebendo valores do channel até que ele seja esvaziado. A recepção de um channel fechado retorna o valor zero do tipo do channel e um boolean false (se usada a forma de duas variáveis).
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
// Receber valores até o channel ser esvaziado
for i := 0; i < 3; i++ {
val, ok := <-ch
fmt.Printf("Valor: %d, Aberto: %t\n", val, ok)
}
}
No exemplo, após fechar o channel, o terceiro recebimento retorna 0 (valor zero de int) e ok = false, indicando que o channel está fechado e vazio.
range em channel
A estrutura for range pode ser usada para iterar sobre os valores de um channel até que ele seja fechado. Isso elimina a necessidade de verificar manualmente o estado do channel.
Importante: o loop range só termina quando o channel é fechado. Se o channel nunca for fechado, o loop ficará bloqueado esperando novos valores, causando um deadlock.
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // necessário para o range terminar
}()
for val := range ch {
fmt.Println(val)
}
fmt.Println("Channel fechado, loop terminou.")
}
O padrão é comum quando uma goroutine produtora envia uma série de valores e depois fecha o channel para sinalizar o fim dos dados.
nil channels
Um channel não inicializado (declarado como var ch chan int) tem valor nil. Enviar ou receber de um channel nil bloqueia para sempre, pois o channel nunca estará pronto para comunicação.
Essa característica pode ser usada de forma criativa para desabilitar seletivamente casos em um select. Atribuir nil a um channel faz com que ele nunca seja selecionado, permitindo controlar dinamicamente quais canais estão ativos.
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "um"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "dois"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("Recebido de ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("Recebido de ch2:", msg2)
}
}
}
No exemplo, se quiséssemos desabilitar temporariamente um channel, poderíamos atribuir nil a ele. Por exemplo, após receber de ch1, poderíamos fazer ch1 = nil para que ele não seja mais considerado no select.
Padrões
Existem vários padrões comuns com channels que facilitam a escrita de código concorrente. Vamos explorar alguns:
Fan-in
Fan-in é o padrão onde múltiplas goroutines enviam dados para um único channel, consolidando as saídas. Isso é feito usando um channel multiplexador.
package main
import (
"fmt"
"sync"
)
func producer(id int, out chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 3; i++ {
out <- id*10 + i
}
}
func main() {
ch := make(chan int)
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go producer(i, ch, &wg)
}
go func() {
wg.Wait()
close(ch)
}()
for val := range ch {
fmt.Println(val)
}
}
Fan-out
Fan-out distribui o trabalho de um único channel para múltiplas goroutines workers. Cada worker processa valores concorrentemente.
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for j := range jobs {
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
var wg sync.WaitGroup
for w := 1; w <= 3; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
wg.Wait()
close(results)
for r := range results {
fmt.Println(r)
}
}
Pipeline
Pipeline é uma sequência de estágios conectados por channels, onde cada estágio processa dados e os envia para o próximo.
package main
import "fmt"
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
// Pipeline
for n := range sq(sq(gen(2, 3))) {
fmt.Println(n) // 16, 81 (2^2^2, 3^2^2)
}
}
Esses padrões promovem modularidade, reuso e paralelismo.
Boas práticas
- Sempre feche um channel do lado do produtor, nunca do consumidor.
- Use
for rangepara consumir dados de um channel até que ele seja fechado. - Evite enviar ou receber de um channel
nilsem intenção; use essa propriedade com cuidado em selects dinâmicos. - Utilize
sync.WaitGrouppara coordenar goroutines e fechar channels de forma segura. - Em pipelines, garanta que todos os estágios sejam encerrados corretamente, fechando os channels para evitar deadlocks.
Referências
- Effective Go: Channels
- Go Language Specification: Close
- Go Language Specification: Range clause
- Go Language Specification: Select statements
- Go Blog: Pipelines
- Go Code Walk: Share Memory By Communicating
Exercícios
Escreva uma função que recebe um channel de inteiros e o fecha após enviar todos os números de 1 a 10. Em seguida, use
rangepara imprimir os valores.✓ Resposta:package main import "fmt" func sendNumbers(ch chan int) { for i := 1; i <= 10; i++ { ch <- i } close(ch) } func main() { ch := make(chan int) go sendNumbers(ch) for v := range ch { fmt.Println(v) } }Explique o que acontece se você tentar enviar para um channel fechado. Escreva um pequeno trecho de código que cause pânico e depois trate o pânico com recover.
✓ Resposta:package main import "fmt" func main() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered:", r) } }() ch := make(chan int) close(ch) ch <- 1 // pânico: send on closed channel }Implemente um padrão fan-in usando um channel multiplexador que coleta resultados de duas goroutines que geram números pares e ímpares respectivamente.
✓ Resposta:package main import ( "fmt" "sync" ) func generateEven(out chan<- int, wg *sync.WaitGroup) { defer wg.Done() for i := 0; i < 5; i++ { out <- i * 2 } } func generateOdd(out chan<- int, wg *sync.WaitGroup) { defer wg.Done() for i := 0; i < 5; i++ { out <- i*2 + 1 } } func main() { ch := make(chan int) var wg sync.WaitGroup wg.Add(2) go generateEven(ch, &wg) go generateOdd(ch, &wg) go func() { wg.Wait() close(ch) }() for v := range ch { fmt.Println(v) } }Crie um pipeline de três estágios: o primeiro gera números de 1 a 10, o segundo multiplica cada número por 2, e o terceiro imprime o resultado.
✓ Resposta:package main import "fmt" func generate(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out } func multiply(in <-chan int, factor int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n * factor } close(out) }() return out } func main() { numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} for v := range multiply(generate(numbers...), 2) { fmt.Println(v) } }Usando nil channels, escreva um select que inicialmente escuta dois channels, mas após receber uma mensagem de um deles, desabilita esse channel permanentemente.
✓ Resposta:package main import ( "fmt" "time" ) func main() { ch1 := make(chan string) ch2 := make(chan string) go func() { time.Sleep(1 * time.Second) ch1 <- "um" }() go func() { time.Sleep(2 * time.Second) ch2 <- "dois" }() for i := 0; i < 2; i++ { select { case msg1 := <-ch1: fmt.Println("Recebido de ch1:", msg1) ch1 = nil // desabilita ch1 case msg2 := <-ch2: fmt.Println("Recebido de ch2:", msg2) ch2 = nil // desabilita ch2 } } }