Channels avançado
Esta aula explora conceitos avançados de channels em Go, incluindo fechamento de channels, uso de range para iteração, comportamento de nil channels e padrões comuns como fan-in e fan-out. O aluno aprenderá a controlar o ciclo de vida de channels e a evitar deadlocks.
Channels são um dos pilares da concorrência em Go, permitindo comunicação segura entre goroutines. Nesta aula, vamos aprofundar em tópicos avançados: como fechar um channel corretamente, iterar sobre valores usando range, o comportamento peculiar de nil channels e padrões de design como fan-in e fan-out. Dominar esses conceitos é essencial para escrever programas concorrentes robustos e eficientes.
Vamos começar entendendo por que e como fechar um channel, e como isso se relaciona com o loop range. Em seguida, exploraremos o uso de nil channels para controle de fluxo e, por fim, veremos padrões que combinam múltiplos channels.
Fechando channels
Fechar um channel sinaliza que nenhum valor será enviado através dele. Isso é útil para notificar receivers que a comunicação terminou. O fechamento é feito com a função close(ch). Tentar enviar para um channel fechado causa pânico, enquanto receber de um channel fechado retorna imediatamente o valor zero do tipo, com o segundo valor (ok) sendo false.
Exemplo básico:
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // fecha o channel após enviar
}()
for {
val, ok := <-ch
if !ok {
fmt.Println("channel fechado")
break
}
fmt.Println(val)
}
}É responsabilidade do sender fechar o channel, nunca do receiver. Fechar um channel já fechado também causa pânico. Use defer close(ch) quando for conveniente.
range em channel
O loop range pode iterar sobre um channel, recebendo valores até que o channel seja fechado. Isso simplifica o código, eliminando a necessidade de verificar manualmente o segundo valor de retorno. Importante: sem o fechamento, o range bloquearia indefinidamente, causando deadlock se não houver mais goroutines enviando.
Exemplo:
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
defer close(ch)
for _, s := range []string{"a", "b", "c"} {
ch <- s
}
}()
for s := range ch {
fmt.Println(s)
}
fmt.Println("Fim")
}O loop range é seguro e idiomático para consumir todos os valores de um channel. Lembre-se de sempre fechar o channel no sender para que o receiver saiba quando parar.
nil channels
Um channel não inicializado (var ch chan int) tem valor nil. Operações de envio ou recebimento em um channel nil bloqueiam para sempre. Isso pode ser usado a seu favor para desabilitar seletivamente um channel em um select. Por exemplo, você pode atribuir nil a um channel para ignorá-lo temporariamente.
Exemplo:
package main
import "fmt"
func main() {
var ch chan int // nil channel
// Isso bloquearia para sempre:
// ch <- 1
// <-ch
// Usando select para evitar bloqueio:
select {
case ch <- 1:
fmt.Println("enviado")
default:
fmt.Println("channel nil, pulando")
}
// Atribuindo um channel real:
ch = make(chan int)
go func() {
ch <- 42
close(ch)
}()
fmt.Println(<-ch)
}Outro padrão comum é usar nil para desligar um branch de select dinamicamente, como veremos a seguir.
Padrões
Dois padrões clássicos são fan-in (várias goroutines enviam para um único channel) e fan-out (uma goroutine distribui trabalho para várias). Vamos ver exemplos.
Fan-in
Fan-in combina múltiplos canais de entrada em um único canal de saída. Usamos select para escutar vários canais simultaneamente.
package main
import (
"fmt"
"time"
)
func producer(name string, ch chan<- string) {
for i := 0; i < 3; i++ {
ch <- fmt.Sprintf("%s: %d", name, i)
time.Sleep(100 * time.Millisecond)
}
close(ch)
}
func fanIn(ch1, ch2 <-chan string) <-chan string {
out := make(chan string)
go func() {
defer close(out)
for ch1 != nil || ch2 != nil {
select {
case val, ok := <-ch1:
if ok {
out <- val
} else {
ch1 = nil // desabilita este case
}
case val, ok := <-ch2:
if ok {
out <- val
} else {
ch2 = nil
}
}
}
}()
return out
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go producer("A", ch1)
go producer("B", ch2)
for val := range fanIn(ch1, ch2) {
fmt.Println(val)
}
}Note como usamos nil para desabilitar canais que já foram fechados, evitando deadlock.
Fan-out
Fan-out distribui mensagens de um canal para múltiplos workers. Cada worker processa em paralelo.
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d processando %d\n", id, job)
}
}
func main() {
const numJobs = 10
const numWorkers = 3
jobs := make(chan int, 100)
var wg sync.WaitGroup
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go worker(i, jobs, &wg)
}
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
wg.Wait()
}Outros padrões incluem pipeline (encadeamento de canais) e uso de context para cancelamento, que veremos em aulas futuras.
Boas práticas
- Sempre feche um channel do lado do sender, nunca do receiver.
- Use
defer close(ch)para garantir fechamento mesmo em caso de pânico. - Evite enviar para um channel fechado; verifique com
okou useselect. - Para iteração completa, prefira
rangesobre um channel. - Use
nilchannels para desabilitar branches deselectdinamicamente.
Referências
- Effective Go: Channels
- Go Language Specification: Close
- Go Language Specification: Range clause
- Go Blog: Pipelines and cancellation
- Go by Example: Non-blocking channel operations
- Go by Example: Worker pools
Exercícios
- Crie um programa que tenha uma goroutine que envia números de 1 a 10 para um channel e depois o fecha. A goroutine principal deve usar
rangepara ler e imprimir os números. - Escreva um programa que demonstre o comportamento de um channel
nildentro de umselect, fazendo com que ele nunca seja escolhido. - Implemente um fan-in que coleta resultados de duas goroutines que calculam quadrados e cubos de números de 1 a 5, respectivamente, e imprime os resultados.
- Crie um programa que use um pipeline: uma goroutine gera números de 1 a 10, outra os dobra, e a main imprime. Use canais para conectar as etapas.
- Implemente um worker pool com 3 workers que processam tarefas de um canal de jobs (strings). Cada worker imprime a tarefa. Use um WaitGroup para aguardar a conclusão.
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
defer close(ch)
for i := 1; i <= 10; i++ {
ch <- i
}
}()
for n := range ch {
fmt.Println(n)
}
}package main
import "fmt"
func main() {
var nilCh chan int
realCh := make(chan int)
go func() {
realCh <- 42
close(realCh)
}()
select {
case val := <-nilCh:
fmt.Println("recebido de nilCh:", val)
case val := <-realCh:
fmt.Println("recebido de realCh:", val)
}
}package main
import "fmt"
func squares(ch chan<- int) {
for i := 1; i <= 5; i++ {
ch <- i * i
}
close(ch)
}
func cubes(ch chan<- int) {
for i := 1; i <= 5; i++ {
ch <- i * i * i
}
close(ch)
}
func fanIn(ch1, ch2 <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for ch1 != nil || ch2 != nil {
select {
case val, ok := <-ch1:
if ok {
out <- val
} else {
ch1 = nil
}
case val, ok := <-ch2:
if ok {
out <- val
} else {
ch2 = nil
}
}
}
}()
return out
}
func main() {
sq := make(chan int)
cu := make(chan int)
go squares(sq)
go cubes(cu)
for val := range fanIn(sq, cu) {
fmt.Println(val)
}
}package main
import "fmt"
func generate(out chan<- int) {
for i := 1; i <= 10; i++ {
out <- i
}
close(out)
}
func double(in <-chan int, out chan<- int) {
for n := range in {
out <- n * 2
}
close(out)
}
func main() {
gen := make(chan int)
doub := make(chan int)
go generate(gen)
go double(gen, doub)
for n := range doub {
fmt.Println(n)
}
}package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan string, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d: %s\n", id, job)
}
}
func main() {
jobs := make(chan string, 10)
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, jobs, &wg)
}
for _, job := range []string{"tarefa1", "tarefa2", "tarefa3", "tarefa4", "tarefa5"} {
jobs <- job
}
close(jobs)
wg.Wait()
}