Go: Comunicação por canal de transmissão

Os canais são os elementos fundamentais do modelo de concorrência do Go — “Não se comunique compartilhando memória; compartilhe memória ao se comunicar.” Os canais tornam a transferência de dados entre goroutines tão segura e elegante quanto o uso de tubos.

Se as goroutines são as “pessoas” da concorrência em Go, então os canais são as “linhas telefônicas” que as conectam. Nesta lição, você vai dominar todos os principais usos dos canais e aprender sobre as armadilhas mais comuns.

1. Você aprenderá


2. A história real de um engenheiro de backend

(1) Problema: O código de memória compartilhada + mutex é desorganizado e propenso a deadlocks

Alice é engenheira de backend na equipe de pagamentos e precisa transmitir dados de transações entre várias goroutines:

“Usei sync.Mutex para proteger uma fatia compartilhada []Transaction, com 5 goroutines gravando nela e 3 goroutines lendo dela. De 100 linhas de código, 30 são instruções de bloqueio/desbloqueio. Quando o revisor viu isso, disse imediatamente: ‘Não posso revisar esse código — não ousaria mexer em uma única linha dele’.”

O código dela é assim:

GO
// Shared memory approach: lock on every read/write, hard to maintain
type TransactionPool struct {
    mu     sync.Mutex
    items  []Transaction
}

func (p *TransactionPool) Add(t Transaction) {
    p.mu.Lock()
    defer p.mu.Unlock()
    p.items = append(p.items, t)
}

func (p *TransactionPool) Get() Transaction {
    p.mu.Lock()
    defer p.mu.Unlock()
    if len(p.items) == 0 {
        return Transaction{}
    }
    item := p.items[0]
    p.items = p.items[1:]
    return item
}

(2) Solução em Go: Passagem de dados por meio de um canal

GO
// channel_approach.go
package main

import "fmt"

type Transaction struct {
    ID     string
    Amount float64
}

func main() {
    // Create an unbuffered channel
    ch := make(chan Transaction)

    // Producer goroutine
    go func() {
        tx := Transaction{ID: "TXN-001", Amount: 99.99}
        ch <- tx  // Send (blocks until receiver is ready)
        fmt.Println("Producer: send complete")
    }()

    // Consumer
    tx := <-ch  // Receive (blocks until sender is ready)
    fmt.Printf("Consumer: received %s ($%.2f)\n", tx.ID, tx.Amount)
}

Resultado:

TEXT 📖 Somente leitura
Consumer: received TXN-001 ($99.99)
Producer: send complete

(3) Desempenho: Canal x Memória Compartilhada

Dimensão Memória compartilhada + Mutex Canal
Tamanho do código 30 linhas de código de modelo 1 linha ch <- v
Segurança de threads Bloqueio/desbloqueio manual Garantias nativas da linguagem
Acoplamento Alto (acoplado por meio de variáveis compartilhadas) Baixo (depende apenas da interface chan)
Testabilidade Requer um Mutex simulado Testar diretamente usando um canal
Risco de impasse Alto (problemas de ordenação de bloqueios) Alguns impasses podem ser detectados em tempo de compilação
💡 Dica: Provérbio do Go: “Não se comunique compartilhando memória; compartilhe memória ao se comunicar.” — Não compartilhe dados; em vez disso, passe os dados de uma goroutine para outra por meio de canais, garantindo que cada dado tenha apenas um proprietário.


3. Criação de canais e operações básicas

(1) Criar um canal

GO
package main

import "fmt"

func main() {
    // Unbuffered channel (synchronous)
    ch1 := make(chan int)

    // Buffered channel (asynchronous, capacity 3)
    ch2 := make(chan string, 3)

    // nil channel (cannot be used directly)
    var ch3 chan float64

    fmt.Printf("ch1: %T, unbuffered\n", ch1)
    fmt.Printf("ch2: %T, buffer=%d\n", ch2, cap(ch2))
    fmt.Printf("ch3: %T, nil=%v\n", ch3, ch3 == nil)
}

(2) Envio e recebimento

GO
ch := make(chan int)

// Send: ch <- value
ch <- 42  // Unbuffered: blocks until someone receives

// Receive: value := <-ch
value := <-ch  // Blocks until someone sends

// Discard received value
<-ch  // Receive only, ignore value (used for synchronization signals)

(3) ▶ Exemplo: Comportamento de sincronização de um canal sem buffer

GO
package main

import (
    "fmt"
    "time"
)

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

    go func() {
        fmt.Println("goroutine: ready to send...")
        ch <- "hello"  // Blocks until main receives
        fmt.Println("goroutine: send complete")
    }()

    time.Sleep(1 * time.Second)  // Simulate delay
    fmt.Println("main: ready to receive...")
    msg := <-ch
    fmt.Printf("main: received %q\n", msg)
}
▶ Experimente

Resultado:

TEXT 📖 Somente leitura
goroutine: ready to send...
main: ready to receive...
goroutine: send complete
main: received "hello"
🔥 Erro comum (canais síncronos sem buffer): O remetente e o destinatário devem estar prontos ao mesmo tempo; caso contrário, um dos lados ficará bloqueado. Canais sem buffer = síncronos — o remetente aguarda o destinatário, e o destinatário aguarda o remetente; ambos só continuam simultaneamente depois de “estremecerem as mãos”.


4. Canais sem buffer x canais com buffer

(1) Comportamento assíncrono dos canais com buffer

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int, 3)  // Capacity 3

    go func() {
        for i := 1; i <= 5; i++ {
            ch <- i
            fmt.Printf("Sent %d (len=%d)\n", i, len(ch))
        }
        close(ch)
    }()

    time.Sleep(500 * time.Millisecond)

    for v := range ch {
        fmt.Printf("Received %d\n", v)
        time.Sleep(200 * time.Millisecond)
    }
}

Resultado:

TEXT 📖 Somente leitura
Sent 1 (len=0)
Sent 2 (len=1)
Sent 3 (len=2)
Sent 4 (len=3)
Sent 5 (len=4)
Received 1
Received 2
Received 3
Received 4
Received 5

(2) ▶ Exemplo: Bloquear quando o buffer estiver cheio

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int, 2)  // Capacity 2

    ch <- 1  // ✅ Does not block (has space)
    ch <- 2  // ✅ Does not block (has space)
    // ch <- 3  // ❌ Blocks! Buffer is full, waiting for receiver

    go func() {
        time.Sleep(100 * time.Millisecond)
        fmt.Println("goroutine: starting to receive")
        <-ch  // Frees up one slot
    }()

    fmt.Println("main: sending 3")
    ch <- 3  // Now does not block (goroutine has received one)
    fmt.Println("main: send complete")
    close(ch)

    for v := range ch {
        fmt.Printf("v=%d\n", v)
    }
}
▶ Experimente

(3) Comparação entre o método sem buffer e o método com buffer

Matéria Sem buffer make(chan T) Com buffer make(chan T, n)
Operação de envio Bloquear até que o destinatário esteja pronto Não bloquear se houver espaço no buffer
Comportamento de recepção Bloquear até que o remetente esteja pronto Não bloquear quando houver dados no buffer
Síncrono/Assíncrono Síncrono (Handshake) Assíncrono (Fila)
Capacidade 0 n
Cenários típicos Sinais de sincronização, coordenação entre goroutines Filas de tarefas, pipelines

5. Iteração por intervalo e proximidade

(1) fechar: Fechar o canal

GO
package main

import "fmt"

func main() {
    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    close(ch)  // Cannot send after closing

    // Can still receive remaining data after closing
    v1 := <-ch  // 1
    v2 := <-ch  // 2
    v3 := <-ch  // 0 (zero value, ok=false)
    fmt.Println(v1, v2, v3)
}

(2) ▶ Exemplo: o comando “comma-ok” verifica se o canal está fechado

GO
package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 10
    ch <- 20
    close(ch)

    // comma-ok: ok=false means channel is closed and has no data
    v, ok := <-ch
    fmt.Printf("v=%d, ok=%v\n", v, ok)  // v=10, ok=true

    v, ok = <-ch
    fmt.Printf("v=%d, ok=%v\n", v, ok)  // v=20, ok=true

    v, ok = <-ch
    fmt.Printf("v=%d, ok=%v\n", v, ok)  // v=0, ok=false (closed and empty)
}
▶ Experimente

(3) Percorrer o canal usando um loop “for-range”

GO
package main

import "fmt"

func main() {
    ch := make(chan string, 3)
    ch <- "Alice"
    ch <- "Bob"
    ch <- "Charlie"
    close(ch)

    // for range automatically exits when the channel is closed
    for name := range ch {
        fmt.Printf("Hello, %s!\n", name)
    }

    // Equivalent to:
    // for {
    //     name, ok := <-ch
    //     if !ok { break }
    //     fmt.Println(name)
    // }
}
🔥 Erro comum: Enviar dados para um canal fechado causará um panic. Fechar um canal que já está fechado também causará um panic. O receptor não precisa se preocupar em fechar o canal — for range cuida disso automaticamente. O remetente é sempre responsável por chamar close.


6. Restrições de sentido

(1) O parâmetro da função especifica a direção do canal

GO
package main

import "fmt"

// Write-only channel (can only send)
func producer(out chan<- int) {
    for i := 1; i <= 3; i++ {
        out <- i  // ✅ Can only send
    }
    close(out)
}

// Read-only channel (can only receive)
func consumer(in <-chan int) {
    for v := range in {
        fmt.Printf("Received: %d\n", v)  // ✅ Can only receive
    }
}

func main() {
    ch := make(chan int, 3)
    go producer(ch)   // Automatically converts to chan<- int
    consumer(ch)       // Automatically converts to <-chan int
}

(2) ▶ Exemplo: Restrições direcionais na prática (padrão Pipeline)

GO
package main

import (
    "fmt"
    "strings"
)

// Stage 1: Write-only
func stage1(names []string, out chan<- string) {
    for _, name := range names {
        out <- strings.ToUpper(name)
    }
    close(out)
}

// Stage 2: Read-write (read from in, process, write to out)
func stage2(in <-chan string, out chan<- string) {
    for name := range in {
        out <- "Hello, " + name + "!"
    }
    close(out)
}

// Stage 3: Read-only
func stage3(in <-chan string) {
    for msg := range in {
        fmt.Println(msg)
    }
}

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

    names := []string{"Alice", "Bob", "Charlie"}

    go stage1(names, ch1)
    go stage2(ch1, ch2)
    stage3(ch2)
}
▶ Experimente

Resultado:

TEXT 📖 Somente leitura
Hello, ALICE!
Hello, BOB!
Hello, CHARLIE!

(3) Uso com restrições direcionais

declarar Permissões Finalidade
ch chan T Leitura/Gravação Declaração de variáveis
ch chan<- T Somente gravação Argumento da função produtora
ch <-chan T Somente leitura Argumento de função de consumidor
💡 Dica: A direcionalidade é verificada apenas em tempo de compilação — passar um canal bidirecional para um parâmetro somente de gravação não causará um erro (devido à conversão implícita automática), mas tentar receber dados em um canal somente de gravação resultará em um erro em tempo de compilação.


7. Introdução à multiplexação seletiva

(1) Noções básicas do SELECT

GO
package main

import (
    "fmt"
    "time"
)

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

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "from ch1"
    }()

    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "from ch2"
    }()

    select {
    case msg := <-ch1:
        fmt.Println(msg)
    case msg := <-ch2:
        fmt.Println(msg)
    case <-time.After(300 * time.Millisecond):
        fmt.Println("Timeout")
    }
}
💡 Dica: select é semelhante a switch, mas é usado para canais — o canal de um case que ficar pronto primeiro é executado. Se vários canais ficarem prontos ao mesmo tempo, um deles é escolhido aleatoriamente. select é a ferramenta definitiva para programação concorrente em Go; abordaremos isso em detalhes na Lição 15.

(2) ▶ Exemplo: Implementação do controle de tempo limite em instruções SELECT

GO
package main

import (
    "fmt"
    "time"
)

func longOperation(result chan<- string) {
    time.Sleep(3 * time.Second)
    result <- "Done"
}

func main() {
    result := make(chan string)

    go longOperation(result)

    select {
    case res := <-result:
        fmt.Println(res)
    case <-time.After(1 * time.Second):
        fmt.Println("Operation timed out!")
    }
}
▶ Experimente

Resultado:

TEXT 📖 Somente leitura
Operation timed out!

8. Cenários comuns de impasse

GO
// Deadlock 1: Unbuffered channel with send but no receive
func deadlock1() {
    ch := make(chan int)
    ch <- 42  // fatal error: all goroutines are asleep - deadlock!
}

// Deadlock 2: main goroutine waiting on itself
func deadlock2() {
    ch := make(chan int)
    <-ch  // fatal error: deadlock
}

// Deadlock 3: Multiple goroutines waiting on each other
func deadlock3() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        <-ch1  // Wait for ch1
        ch2 <- 1
    }()

    <-ch2  // Wait for ch2
    ch1 <- 1  // Never reaches this line
}

(1) Referência rápida: 4 tipos de cenários de impasse

Cenário Causa Solução
Apenas envio, sem recepção Bloqueio de canal sem buffer Certifique-se de que haja consumidores
Apenas recepção, sem envio O canal está vazio e não há remetente Certifique-se de que haja um produtor
Goroutines que aguardam umas pelas outras A aguarda o canal de B, B aguarda o canal de A Redesenhando a ordem de dependência
Operações em canais nulos Bloqueio permanente em um canal nulo ou a partir dele Inicialização de um canal

9. Exemplo completo: Pipeline produtor-consumidor

GO
// pipeline.go
package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

// Work unit
type Job struct {
    ID      int
    Payload string
}

type Result struct {
    Job      Job
    Output   string
    Err      error
    Duration time.Duration
}

// Producer: generates jobs (write-only channel)
func producer(jobs chan<- Job, count int) {
    for i := 1; i <= count; i++ {
        jobs <- Job{
            ID:      i,
            Payload: fmt.Sprintf("task-%d", i),
        }
        time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond)
    }
    close(jobs)
}

// Consumer: processes jobs (read jobs, write results)
func consumer(id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        start := time.Now()

        // Simulate processing
        time.Sleep(time.Duration(50+rand.Intn(100)) * time.Millisecond)
        output := fmt.Sprintf("consumer-%d processed %s", id, job.Payload)

        results <- Result{
            Job:      job,
            Output:   output,
            Duration: time.Since(start),
        }
    }
}

// Collector: collects and prints results (read-only channel)
func collector(results <-chan Result, done chan<- struct{}) {
    var total time.Duration
    count := 0

    for r := range results {
        count++
        total += r.Duration
        status := "OK"
        if r.Err != nil {
            status = "ERR"
        }
        fmt.Printf("[%s] Job#%d: %s (%v)\n",
            status, r.Job.ID, r.Output, r.Duration)
    }

    if count > 0 {
        fmt.Printf("\nTotal %d tasks, avg %.2f ms\n",
            count, float64(total.Milliseconds())/float64(count))
    }
    close(done)
}

func main() {
    const (
        jobCount     = 20
        workerCount  = 3
    )

    jobs := make(chan Job, 10)
    results := make(chan Result, 10)
    done := make(chan struct{})

    // 1 producer
    go producer(jobs, jobCount)

    // N consumers
    var wg sync.WaitGroup
    for w := 1; w <= workerCount; w++ {
        wg.Add(1)
        go consumer(w, jobs, results, &wg)
    }

    // Wait for all consumers to finish, then close results
    go func() {
        wg.Wait()
        close(results)
    }()

    // Collector (main goroutine)
    collector(results, done)
    <-done
}

Resultado esperado:

TEXT 📖 Somente leitura
[OK] Job#1: consumer-2 processed task-1 (85ms)
[OK] Job#2: consumer-1 processed task-2 (92ms)
[OK] Job#3: consumer-3 processed task-3 (78ms)
...
[OK] Job#20: consumer-1 processed task-20 (112ms)

Total 20 tasks, avg 87.45 ms
100%
sequenceDiagram
    participant P as Producer
    participant C as Channel
    participant W as Consumer

    Note over P: Sender is responsible for close
    P->>C: ch <- job (send)
    activate C
    Note over C: Unbuffered: sync wait<br/>Buffered: enqueue
    C->>W: job := <-ch (receive)
    deactivate C
    Note over W: Process task
    W-->>P: Continue loop
    Note over P: close(ch)
    Note over C: Remaining data readable
    Note over W: for range auto-exit
🔥 Erro comum: Este exemplo possui 3 canais (jobs/results/done) e 3 tipos de restrições direcionais (somente gravação, somente leitura, leitura e gravação). As restrições direcionais garantem, em tempo de compilação, que não seja possível receber dados no produtor — essa é uma característica do sistema de tipos do Go para garantir a segurança da concorrência.


❓ Perguntas Frequentes

P: Qual é a diferença entre canais com buffer e sem buffer? R: Nos canais sem buffer, o envio e o recebimento devem estar prontos ao mesmo tempo (handshake síncrono); caso contrário, o canal fica bloqueado. Um canal com buffer não fica bloqueado durante o envio se o buffer não estiver cheio, e não fica bloqueado durante o recebimento se o buffer não estiver vazio. Canais sem buffer são usados para coordenação síncrona, enquanto canais com buffer são usados para filas assíncronas.

P: Ainda é possível enviar mensagens depois que um canal é fechado? R: Não. Enviar uma mensagem para um canal fechado causará um erro grave. Após o fechamento, você pode continuar recebendo os dados restantes; assim que todos os dados forem lidos, um valor zero será retornado. Em v, ok := <-ch, um valor ok igual a false indica que o canal está fechado e não há mais dados.

P: Qual é a finalidade das restrições direcionais chan<- e <-chan? R: Elas restringem a direção dos canais nas assinaturas de funções — garantindo, em tempo de compilação, que os produtores não recebam acidentalmente e que os consumidores não enviem acidentalmente. O compilador Go reportará um erro em tempo de compilação, em vez de causar um panic em tempo de execução.

P: Qual é a implementação subjacente de um canal? R: Por trás dos bastidores, um canal consiste em um buffer circular mais duas filas de espera (a fila do remetente e a fila do destinatário). Quando os dados são enviados, se houver destinatários aguardando na fila do destinatário, eles são repassados diretamente; caso contrário, são colocados no buffer ou na fila do remetente. Consulte a estrutura hchan no código-fonte do Go runtime/chan.go.

P: Como o select utiliza os canais? R: O select monitora vários canais simultaneamente; ele executa o case para o canal que ficar pronto primeiro. Se vários canais ficarem prontos ao mesmo tempo, ele seleciona um aleatoriamente. Se nenhum dos canais estiver pronto e um caso default for especificado, o caso default é executado; caso contrário, o processo é bloqueado e aguarda. select é fundamental para o controle de tempo limite e operações não bloqueantes.

P: Como evitar impasses em canais? R: Quatro regras: canais sem buffer devem ter um receptor correspondente; preste atenção à ordem das dependências entre goroutines; use select com uma cláusula default para implementar operações não bloqueantes; use close para notificar o receptor de que o canal foi fechado.

P: Um canal pode ser usado como um semáforo? R: Sim. Um canal com buffer pode funcionar como um semáforo: inicialize sem := make(chan struct{}, 10), envie sem <- struct{}{} antes da operação e receba <-sem após a conclusão. Ele bloqueia quando o buffer está cheio — proporcionando uma limitação natural da taxa de transferência.

P: Quando se deve usar um canal e quando se deve usar um mutex? R: Transferência de dados → canal; proteção de dados → mutex. Especificamente: transferência de propriedade (dados de uma goroutine para outra) → canal; proteção de estado compartilhado (várias goroutines acessando a mesma variável) → mutex. Os dois não são substitutos um do outro; são complementares.


📖 Resumo


📝 Exercícios

  1. Problema básico (Dificuldade ⭐): Escreva um programa que inicie uma goroutine para enviar os dez dígitos de 0 a 9 para um canal sem buffer, enquanto a goroutine principal os recebe e os exibe. Observe a alternância entre o envio e o recebimento.

  2. Problema avançado (Dificuldade ⭐⭐): Implemente um padrão fan-out: um produtor envia 100 tarefas para um canal; inicie 5 goroutines consumidoras para ler e processar tarefas do mesmo canal, com cada consumidora exibindo seu próprio ID e o número da tarefa. Você deve usar um canal com buffer, close e for range.

  3. Desafio (Dificuldade ⭐⭐⭐): Implemente um pipeline de três estágios: o Estágio 1 gera números inteiros aleatórios ([]int); o Estágio 2 filtra os números pares; o Estágio 3 calcula a soma dos quadrados. Cada etapa é uma goroutine independente conectada por meio de canais. Requisitos: (1) Use restrições direcionais entre as etapas; (2) Permita o ajuste dinâmico do número de etapas; (3) Implemente uma saída elegante usando select.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%