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 range para consumir dados de um channel até que ele seja fechado.
  • Evite enviar ou receber de um channel nil sem intenção; use essa propriedade com cuidado em selects dinâmicos.
  • Utilize sync.WaitGroup para 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

Exercícios

  1. 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 range para 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)
        }
    }
    
  2. 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
    }
    
  3. 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)
        }
    }
    
  4. 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)
        }
    }
    
  5. 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
            }
        }
    }