Channels: comunicando goroutines com segurança

1. O Problema da Comunicação entre Goroutines

Quando trabalhamos com concorrência em Go, um dos maiores desafios é coordenar o acesso a dados compartilhados entre múltiplas goroutines. Sem uma estratégia adequada, somos rapidamente confrontados com condições de corrida (race conditions), onde o resultado da execução depende da ordem imprevisível de acesso concorrente.

Tradicionalmente, linguagens de programação resolvem esse problema com locks, mutexes e memória compartilhada. Go, porém, adota uma filosofia diferente. No famoso slogan do Go:

"Não se comunique compartilhando memória; compartilhe memória se comunicando."

Isso significa que, em vez de proteger regiões críticas com locks, devemos passar dados entre goroutines através de canais de comunicação — os channels. Essa abordagem elimina uma vasta gama de bugs relacionados à concorrência, pois cada dado pertence a apenas uma goroutine por vez.

2. Fundamentos de Channels em Go

Um channel em Go é um tubo tipado através do qual você pode enviar e receber valores. Sua sintaxe é simples:

// Channel unbuffered (sem buffer)
ch := make(chan int)

// Channel buffered (com buffer de capacidade 3)
chBuf := make(chan string, 3)

O operador <- é usado tanto para enviar quanto para receber:

ch <- 42        // Enviar 42 para o channel
valor := <-ch   // Receber um valor do channel

A diferença fundamental entre unbuffered e buffered está no comportamento de sincronização.

3. Channels Unbuffered: Sincronização Pura

Um channel unbuffered não possui capacidade de armazenamento. Isso significa que cada envio bloqueia até que haja uma goroutine receptora pronta, e vice-versa. Esse comportamento cria um handshake entre as goroutines.

Veja um exemplo clássico de produtor-consumidor:

func produtor(ch chan<- int) {
    for i := 1; i <= 5; i++ {
        fmt.Printf("Produzindo: %d\n", i)
        ch <- i // Bloqueia até o consumidor receber
    }
    close(ch)
}

func consumidor(ch <-chan int) {
    for valor := range ch {
        fmt.Printf("Consumindo: %d\n", valor)
        time.Sleep(100 * time.Millisecond) // Simula trabalho
    }
}

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

Aqui, cada item só é produzido quando o anterior é consumido. Isso garante sincronização sem locks explícitos.

4. Channels com Buffer: Tolerância a Atrasos

Channels buffered possuem uma capacidade definida. Enquanto o buffer não estiver cheio, o envio não bloqueia. Isso permite desacoplamento temporal entre produtor e consumidor.

func main() {
    ch := make(chan string, 3)

    // Podemos enviar até 3 itens sem receptor
    ch <- "msg1"
    ch <- "msg2"
    ch <- "msg3"

    fmt.Println(<-ch) // msg1
    fmt.Println(<-ch) // msg2
    fmt.Println(<-ch) // msg3
}

Cuidado: buffer cheio causa bloqueio no envio; buffer vazio causa bloqueio na recepção. Deadlocks são comuns quando a capacidade é mal dimensionada.

5. Padrões de Fechamento e Range

A função close() sinaliza que nenhum valor será enviado. Isso permite que receptores usem for range para consumir até o fim:

func main() {
    ch := make(chan int)

    go func() {
        for i := 0; i < 3; i++ {
            ch <- i
        }
        close(ch) // Sinaliza fim dos dados
    }()

    for valor := range ch {
        fmt.Println(valor) // 0, 1, 2
    }
}

Para verificar se um channel foi fechado:

valor, ok := <-ch
if !ok {
    fmt.Println("Channel fechado")
}

6. Select: Multiplexando Múltiplos Channels

O select permite que uma goroutine aguarde múltiplas operações de channel simultaneamente:

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "resposta lenta"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "resposta mais lenta"
    }()

    select {
    case msg1 := <-ch1:
        fmt.Println("Recebido de ch1:", msg1)
    case msg2 := <-ch2:
        fmt.Println("Recebido de ch2:", msg2)
    case <-time.After(500 * time.Millisecond):
        fmt.Println("Timeout! Nenhuma resposta em 500ms")
    }
}

Padrões úteis com select:
- Timeout: case <-time.After(duration)
- Cancelamento: combinado com context.Context
- Operação não-bloqueante: default: case

7. Padrões Avançados com Channels

Fan-in: Múltiplas fontes, um destino

func fanIn(ch1, ch2 <-chan string) <-chan string {
    ch := make(chan string)
    go func() {
        for {
            select {
            case v := <-ch1:
                ch <- v
            case v := <-ch2:
                ch <- v
            }
        }
    }()
    return ch
}

Fan-out: Um produtor, múltiplos workers

func worker(id int, jobs <-chan int, results chan<- int) {
    for job := range jobs {
        results <- job * 2 // Processa o job
    }
}

func main() {
    jobs := make(chan int, 10)
    results := make(chan int, 10)

    // Inicia 3 workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Envia 5 jobs
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)

    // Coleta resultados
    for r := 1; r <= 5; r++ {
        <-results
    }
}

Pipeline: Encadeamento de stages

func main() {
    // Stage 1: gera números
    numeros := make(chan int)
    go func() {
        for i := 1; i <= 5; i++ {
            numeros <- i
        }
        close(numeros)
    }()

    // Stage 2: dobra os números
    dobrados := make(chan int)
    go func() {
        for n := range numeros {
            dobrados <- n * 2
        }
        close(dobrados)
    }()

    // Stage 3: consome
    for d := range dobrados {
        fmt.Println(d)
    }
}

8. Boas Práticas e Armadilhas Comuns

❌ Nunca enviar em channel fechado

ch := make(chan int)
close(ch)
ch <- 42 // PANIC: send on closed channel

❌ Goroutine leaks

Sempre garanta que goroutines que enviam para channels sejam finalizadas. Use close() ou cancele com context:

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // Garante que a goroutine termine

✅ Preferir channels tipados

Evite chan interface{}. Channels tipados trazem segurança de tipo e melhor legibilidade:

// Ruim
ch := make(chan interface{})
ch <- "texto"
valor := <-ch // Precisa de type assertion

// Bom
ch := make(chan string)
ch <- "texto"
valor := <-ch // Tipo garantido

✅ Documentar a direção do channel

Use chan<- (send-only) e <-chan (receive-only) em parâmetros de função para deixar explícita a intenção.

Referências