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 ok ou use select.
  • Para iteração completa, prefira range sobre um channel.
  • Use nil channels para desabilitar branches de select dinamicamente.

Referências

Exercícios

  1. 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 range para ler e imprimir os números.
  2. ✓ Resposta:
    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)
        }
    }
  3. Escreva um programa que demonstre o comportamento de um channel nil dentro de um select, fazendo com que ele nunca seja escolhido.
  4. ✓ Resposta:
    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)
        }
    }
  5. 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.
  6. ✓ Resposta:
    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)
        }
    }
  7. 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.
  8. ✓ Resposta:
    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)
        }
    }
  9. 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.
  10. ✓ Resposta:
    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()
    }