Channels são um mecanismo de comunicação entre goroutines em Go. Eles permitem que uma goroutine envie dados para outra, garantindo sincronização e segurança em concorrência. Nesta aula, veremos como criar, usar e controlar channels, incluindo canais com buffer e direção de canais.

Channels são tipados e podem ser unidirecionais ou bidirecionais. Eles são fundamentais para o modelo de concorrência do Go, evitando condições de corrida e simplificando a lógica de programas concorrentes.

Criação

Um channel é criado com a função make, especificando o tipo de dado que será transportado. A sintaxe é make(chan Tipo) para canais sem buffer e make(chan Tipo, capacidade) para canais com buffer.

Exemplo:

ch := make(chan int) // canal sem buffer
chBuf := make(chan string, 10) // canal com buffer de capacidade 10

Channels são referências; quando passados para funções, compartilham o mesmo canal. O zero de um channel é nil, e enviar ou receber de um canal nil bloqueia para sempre.

send e receive

Para enviar um valor para um channel, usa-se a sintaxe ch <- valor. Para receber, usa-se valor := <-ch. O envio e recebimento são operações bloqueantes em canais sem buffer: a goroutine que envia espera até que outra goroutine receba, e vice-versa.

Exemplo:

package main

import "fmt"

func main() {
    ch := make(chan string)
    go func() {
        ch <- "Olá, mundo!"
    }()
    msg := <-ch
    fmt.Println(msg)
}

Neste exemplo, a goroutine anônima envia uma string para o canal, e a main recebe. A sincronização ocorre naturalmente.

Channels com buffer

Channels com buffer têm uma capacidade definida. O envio só bloqueia quando o buffer está cheio, e o recebimento só bloqueia quando o buffer está vazio. Isso permite que o produtor envie vários itens sem esperar imediatamente pelo consumidor.

Exemplo:

package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    // ch <- 3 // isso bloquearia, pois buffer está cheio
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

Neste código, o buffer de capacidade 2 permite enviar dois valores sem bloqueio. O terceiro envio causaria deadlock se não houver consumidor.

Direção de channel

Channels podem ser declarados como unidirecionais: apenas para envio (chan<- Tipo) ou apenas para recebimento (<-chan Tipo). Isso é útil para restringir o uso em funções, aumentando a segurança.

Exemplo:

package main

import "fmt"

func produtor(ch chan<- int) {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}

func consumidor(ch <-chan int) {
    for valor := range ch {
        fmt.Println(valor)
    }
}

func main() {
    ch := make(chan int, 5)
    go produtor(ch)
    consumidor(ch)
}

A função produtor só pode enviar (chan<- int), enquanto consumidor só pode receber (<-chan int). Isso evita usos incorretos.

Boas práticas

Sempre feche um canal quando não houver mais envios, para que o receptor saiba que terminou. Use range para iterar sobre um canal até que ele seja fechado. Evite enviar em canais fechados (causa pânico). Prefira canais com buffer quando a taxa de produção e consumo for diferente.

Referências

Exercícios

  1. Crie um canal sem buffer de inteiros. Em uma goroutine, envie o número 42. Na main, receba e imprima.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int)
        go func() { ch <- 42 }()
        fmt.Println(<-ch)
    }
  2. Crie um canal com buffer de capacidade 3. Envie três strings sem usar goroutines e depois receba todas.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan string, 3)
        ch <- "a"
        ch <- "b"
        ch <- "c"
        fmt.Println(<-ch, <-ch, <-ch)
    }
  3. Escreva uma função que recebe um canal de envio de inteiros e envia os números de 1 a 5. Na main, crie um canal com buffer 5, chame a função em uma goroutine e receba os valores usando range.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func produtor(ch chan<- int) {
        for i := 1; i <= 5; i++ {
            ch <- i
        }
        close(ch)
    }
    
    func main() {
        ch := make(chan int, 5)
        go produtor(ch)
        for v := range ch {
            fmt.Println(v)
        }
    }
  4. Crie uma função que recebe um canal de recebimento de strings e imprime cada string recebida. Na main, crie um canal, envie duas strings em goroutines separadas e chame a função.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func consumidor(ch <-chan string) {
        for s := range ch {
            fmt.Println(s)
        }
    }
    
    func main() {
        ch := make(chan string, 2)
        go func() { ch <- "hello" }()
        go func() { ch <- "world" }()
        go consumidor(ch)
        // Aguarda um pouco para ver a saída
        fmt.Scanln()
    }
  5. Explique o que acontece se você enviar em um canal fechado. Escreva um pequeno programa que demonstre isso (pode usar recover).

    ✓ Resposta:Enviar em um canal fechado causa pânico (panic). Exemplo:
    package main
    
    func main() {
        ch := make(chan int)
        close(ch)
        ch <- 1 // panic: send on closed channel
    }