Golang

Channels: comunicação entre goroutines Já leu

9 min de leitura

Channels: comunicação entre goroutines
No artigo anterior, goroutines foram apresentadas como unidades de execução concorrente. Mas goroutines isoladas têm utilidade limitada — o poder real emerge quando elas se comunicam e coordenam trabalho. Channels são o

No artigo anterior, goroutines foram apresentadas como unidades de execução concorrente. Mas goroutines isoladas têm utilidade limitada — o poder real emerge quando elas se comunicam e coordenam trabalho. Channels são o mecanismo que torna isso possível.

Um channel é um conduíte tipado através do qual goroutines enviam e recebem valores. Ele sincroniza automaticamente o acesso — quem envia espera quem recebe, e vice-versa, dependendo do tipo de channel. Isso elimina grande parte da complexidade que torna concorrência difícil em outras linguagens.


Criando e usando channels

Channels são criados com make e tipados pelo valor que transportam:

package main

import "fmt"

func main() {
    // Channel de inteiros sem buffer
    ch := make(chan int)

    // Enviando em uma goroutine separada
    go func() {
        ch <- 42  // envia 42 para o channel
    }()

    // Recebendo na goroutine principal
    valor := <-ch
    fmt.Println(valor) // 42
}

O operador <- é usado tanto para envio quanto para recebimento. A direção da seta indica a direção do dado:

  • ch <- valor — envia valor para o channel ch
  • valor := <-ch — recebe do channel ch e armazena em valor
  • <-ch — recebe e descarta o valor

Channels sem buffer: sincronização garantida

Um channel criado com make(chan T) é sem buffer. Ele não armazena valores — cada envio bloqueia até que haja um receptor, e cada recebimento bloqueia até que haja um emissor. Essa propriedade de rendezvous garante sincronização precisa:

package main

import (
    "fmt"
    "time"
)

func processarPedido(id int, resultado chan<- string) {
    time.Sleep(100 * time.Millisecond) // simula processamento
    resultado <- fmt.Sprintf("pedido %d processado", id)
}

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

    go processarPedido(1, resultado)

    // main bloqueia aqui até processarPedido enviar
    msg := <-resultado
    fmt.Println(msg)
}

Channels com buffer

Um channel com buffer aceita um número definido de valores sem bloquear o emissor. O bloqueio só ocorre quando o buffer está cheio (para emissores) ou vazio (para receptores):

package main

import "fmt"

func main() {
    // Channel com buffer de capacidade 3
    ch := make(chan string, 3)

    // Esses envios não bloqueiam — o buffer absorve os valores
    ch <- "primeiro"
    ch <- "segundo"
    ch <- "terceiro"
    // ch <- "quarto"  — bloquearia: buffer cheio

    fmt.Println(<-ch) // primeiro
    fmt.Println(<-ch) // segundo
    fmt.Println(<-ch) // terceiro

    fmt.Println(len(ch), cap(ch)) // 0 3
}

len(ch) retorna quantos valores estão no buffer atualmente. cap(ch) retorna a capacidade total do buffer.

A escolha entre channel com ou sem buffer depende do problema. Sem buffer: quando a sincronização entre emissor e receptor é necessária. Com buffer: quando se quer desacoplar levemente o ritmo de produção e consumo.


Fechando channels

Um channel pode ser fechado com close(ch). Após o fechamento, nenhum valor pode ser enviado, mas valores já no buffer podem ser recebidos. Receber de um channel fechado e vazio retorna o valor zero do tipo e false:

package main

import "fmt"

func gerarNumeros(ch chan<- int, limite int) {
    for i := 1; i <= limite; i++ {
        ch <- i
    }
    close(ch) // sinaliza que não há mais valores
}

func main() {
    ch := make(chan int, 5)
    go gerarNumeros(ch, 5)

    // Forma 1: verificando se o channel foi fechado
    for {
        valor, aberto := <-ch
        if !aberto {
            break
        }
        fmt.Println(valor)
    }
}

A forma idiomática de iterar até um channel ser fechado usa for range:

ch := make(chan int, 5)
go gerarNumeros(ch, 5)

for valor := range ch {  // termina automaticamente quando ch for fechado
    fmt.Println(valor)
}

Regras importantes sobre fechar channels: apenas o emissor deve fechar o channel, nunca o receptor. Fechar um channel já fechado causa pânico. Enviar para um channel fechado causa pânico.


Direção de channels em funções

Parâmetros de função podem restringir a direção de uso de um channel. Isso documenta a intenção e permite que o compilador detecte usos incorretos:

// chan<- int — apenas envio (send-only)
func produtor(ch chan<- int) {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
    // <-ch  — erro de compilação: receive from send-only channel
}

// <-chan int — apenas recebimento (receive-only)
func consumidor(ch <-chan int) {
    for v := range ch {
        fmt.Println("recebido:", v)
    }
    // ch <- 1  — erro de compilação: send to receive-only channel
}

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

Um channel bidirecional chan T pode ser passado onde chan<- T ou <-chan T são esperados — a conversão é automática. O contrário não é verdade.


Select: múltiplos channels simultaneamente

O select é ao channels o que o switch é a valores — permite aguardar múltiplos channels simultaneamente, executando o case do primeiro que estiver pronto:

package main

import (
    "fmt"
    "time"
)

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

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

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

    // Aguarda o que chegar primeiro
    select {
    case msg := <-ch1:
        fmt.Println("ch1 venceu:", msg)
    case msg := <-ch2:
        fmt.Println("ch2 venceu:", msg)
    }
}

Se múltiplos cases estiverem prontos simultaneamente, Go escolhe um aleatoriamente — garantindo que nenhum case seja perpetuamente ignorado.

O select com default é não bloqueante — se nenhum channel estiver pronto, o default executa imediatamente:

select {
case msg := <-ch:
    fmt.Println("recebido:", msg)
default:
    fmt.Println("nenhuma mensagem disponível agora")
}

Timeout com select e time.After

Implementar timeouts em operações concorrentes é trivial com select e time.After:

package main

import (
    "fmt"
    "time"
)

func operacaoLenta(resultado chan<- string) {
    time.Sleep(2 * time.Second)
    resultado <- "dados processados"
}

func main() {
    resultado := make(chan string, 1)
    go operacaoLenta(resultado)

    select {
    case dados := <-resultado:
        fmt.Println("Sucesso:", dados)
    case <-time.After(500 * time.Millisecond):
        fmt.Println("Timeout: operação demorou demais")
    }
}

time.After retorna um channel que recebe um valor após a duração especificada. Quando esse case vence o select, o timeout foi atingido.


Padrão done channel: cancelamento

Um padrão fundamental em Go é o done channel — um channel usado para sinalizar a outras goroutines que devem encerrar:

package main

import (
    "fmt"
    "time"
)

func trabalhoContínuo(done <-chan struct{}, id int) {
    for {
        select {
        case <-done:
            fmt.Printf("goroutine %d encerrada\n", id)
            return
        default:
            fmt.Printf("goroutine %d trabalhando\n", id)
            time.Sleep(200 * time.Millisecond)
        }
    }
}

func main() {
    done := make(chan struct{})

    for i := 1; i <= 3; i++ {
        go trabalhoContínuo(done, i)
    }

    time.Sleep(600 * time.Millisecond)
    close(done) // fecha o channel — sinaliza todas as goroutines simultaneamente
    time.Sleep(100 * time.Millisecond)
    fmt.Println("todas as goroutines encerradas")
}

Fechar um channel é a forma idiomática de fazer broadcast para múltiplas goroutines — todas as que estão lendo do channel desbloqueiam imediatamente quando ele é fechado. Isso não seria possível com envio, que acorda apenas um receptor por vez.

O tipo struct{} é usado porque ocupa zero bytes — o channel serve apenas como sinal, não transporta dados.


Pipeline: channels em cadeia

O padrão de pipeline conecta goroutines em cadeia, onde a saída de uma é a entrada da próxima:

package main

import "fmt"

// Estágio 1: gera números
func gerar(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

// Estágio 2: eleva ao quadrado
func elevarAoQuadrado(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

// Estágio 3: filtra apenas pares
func filtrarPares(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            if n%2 == 0 {
                out <- n
            }
        }
        close(out)
    }()
    return out
}

func main() {
    // Conecta o pipeline: gerar → quadrado → filtrar pares
    nums := gerar(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    quadrados := elevarAoQuadrado(nums)
    pares := filtrarPares(quadrados)

    for v := range pares {
        fmt.Println(v) // 4, 16, 36, 64, 100
    }
}

Cada estágio do pipeline roda em sua própria goroutine. Os channels garantem que cada estágio só processa um valor quando o próximo está pronto para recebê-lo — backpressure natural.


Fan-out e fan-in

Fan-out distribui trabalho de um channel para múltiplos workers. Fan-in combina resultados de múltiplos channels em um só:

package main

import (
    "fmt"
    "sync"
)

// Fan-out: múltiplos workers leem do mesmo channel
func worker(id int, tarefas <-chan int, resultados chan<- string, wg *sync.WaitGroup) {
    defer wg.Done()
    for tarefa := range tarefas {
        resultados <- fmt.Sprintf("worker %d processou tarefa %d", id, tarefa)
    }
}

// Fan-in: combina múltiplos channels em um
func merge(channels ...<-chan string) <-chan string {
    var wg sync.WaitGroup
    merged := make(chan string, 10)

    enviar := func(ch <-chan string) {
        defer wg.Done()
        for v := range ch {
            merged <- v
        }
    }

    wg.Add(len(channels))
    for _, ch := range channels {
        go enviar(ch)
    }

    go func() {
        wg.Wait()
        close(merged)
    }()

    return merged
}

func main() {
    tarefas := make(chan int, 10)
    resultados := make(chan string, 10)
    var wg sync.WaitGroup

    // Fan-out: 3 workers lendo do mesmo channel de tarefas
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(i, tarefas, resultados, &wg)
    }

    // Envia 9 tarefas
    for i := 1; i <= 9; i++ {
        tarefas <- i
    }
    close(tarefas)

    // Fecha resultados após todos os workers terminarem
    go func() {
        wg.Wait()
        close(resultados)
    }()

    for resultado := range resultados {
        fmt.Println(resultado)
    }
}

Resumo do que foi coberto

Este artigo apresentou channels em Go de forma abrangente: criação e sintaxe do operador <-, channels com e sem buffer, fechamento de channels e iteração com for range, direção de channels em parâmetros de função, select para múltiplos channels, timeouts com time.After, o padrão done channel para cancelamento, pipelines e os padrões fan-out e fan-in. O próximo artigo aprofunda o select e os mecanismos de cancelamento com context.


Referências e leituras complementares

  • Go Blog: Go Concurrency Patterns — Artigo clássico sobre pipelines, fan-out e fan-in. https://go.dev/blog/pipelines

  • Go Blog: Advanced Go Concurrency Patterns — Padrões avançados incluindo done channels. https://go.dev/blog/advanced-go-concurrency-patterns

  • A Tour of Go — Channels — Introdução interativa a channels e select. https://go.dev/tour/concurrency/2

  • Go by Example: Channels — Exemplos práticos comentados. https://gobyexample.com/channels

  • Go by Example: Select — Exemplos do select com múltiplos channels. https://gobyexample.com/select

  • Go by Example: Non-Blocking Channel Operations — O padrão select com default. https://gobyexample.com/non-blocking-channel-operations

Comentários

Mais em Golang

Armadilhas da concorrência: race conditions e como detectá-las
Armadilhas da concorrência: race conditions e como detectá-las

Concorrência bem implementada torna sistemas mais rápidos, mais responsivos e...

Artigo 30 — Consumindo APIs externas com http.Client
Artigo 30 — Consumindo APIs externas com http.Client

Artigo 30 — Consumindo APIs externas com http.Client O cliente que já vem con...

Go com MySQL e MariaDB: conexão, queries e transações
Go com MySQL e MariaDB: conexão, queries e transações

MySQL é o banco de dados relacional mais usado no mundo, e MariaDB é seu fork...