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— enviavalorpara o channelchvalor := <-ch— recebe do channelche armazena emvalor<-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