Channels
Esta aula aborda channels em Go, um mecanismo de comunicação entre goroutines. Você aprenderá a criar channels, enviar e receber dados, usar buffers e controlar a direção dos channels para concorrência segura.
Channels são um dos pilares da concorrência em Go. Eles permitem que goroutines troquem dados de forma segura e sincronizada, evitando condições de corrida. Nesta aula, vamos explorar os fundamentos dos channels, desde sua criação até padrões avançados de uso, com exemplos práticos.
Entender channels é essencial para escrever programas concorrentes eficientes em Go. Eles funcionam como tubos por onde valores fluem, podendo ser usados para sincronizar execuções ou comunicar resultados entre goroutines.
Criação
Um channel em Go é criado com a função make, especificando o tipo de dado que será transmitido. A sintaxe é ch := make(chan Tipo). Por exemplo, ch := make(chan int) cria um channel de inteiros. Channels sem buffer são chamados de unbuffered e exigem que uma goroutine esteja pronta para receber o valor no momento do envio, garantindo sincronização.
Exemplo de criação e uso básico:
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
ch <- 42
}()
valor := <-ch
fmt.Println(valor)
}Neste exemplo, criamos um channel de inteiros, enviamos o valor 42 em uma goroutine separada e o recebemos na main. A operação de envio bloqueia até que o recebimento ocorra.
send e receive
As operações de envio (send) e recebimento (receive) são a base da comunicação via channels. O envio é feito com ch <- valor e o recebimento com valor := <-ch. Em channels unbuffered, essas operações são bloqueantes: um envio espera um recebedor e vice-versa. Isso permite sincronizar goroutines.
Exemplo de sincronização:
package main
import (
"fmt"
"time"
)
func worker(done chan bool) {
fmt.Print("Trabalhando...")
time.Sleep(time.Second)
fmt.Println("pronto")
done <- true
}
func main() {
done := make(chan bool)
go worker(done)
<-done
fmt.Println("Main finalizou")
}Aqui, a goroutine worker sinaliza que terminou enviando true pelo channel. A main aguarda esse sinal, garantindo que o worker termine antes de prosseguir.
Channels com buffer
Channels podem ter um buffer, permitindo enviar valores sem que uma goroutine receptora esteja imediatamente pronta. A criação é ch := make(chan Tipo, capacidade). O buffer armazena até capacidade valores. Enquanto o buffer não estiver cheio, o envio não bloqueia; enquanto não estiver vazio, o recebimento não bloqueia.
Exemplo:
package main
import "fmt"
func main() {
ch := make(chan string, 2)
ch <- "olá"
ch <- "mundo"
fmt.Println(<-ch)
fmt.Println(<-ch)
}Neste caso, enviamos duas strings sem necessidade de um receptor imediato. O buffer comporta 2 valores, então os envios não bloqueiam. Depois, recebemos ambos.
Channels com buffer são úteis para desacoplar produtores e consumidores, permitindo picos de produção sem bloquear. No entanto, é importante dimensionar o buffer adequadamente para evitar deadlocks.
Direção de channel
Go permite especificar a direção de um channel em parâmetros de função: chan<- para apenas envio e <-chan para apenas recebimento. Isso aumenta a segurança de tipo, garantindo que a função não use o channel indevidamente.
Exemplo:
package main
import "fmt"
func ping(pings chan<- string, msg string) {
pings <- msg
}
func pong(pings <-chan string, pongs chan<- string) {
msg := <-pings
pongs <- msg
}
func main() {
pings := make(chan string, 1)
pongs := make(chan string, 1)
ping(pings, "mensagem passada")
pong(pings, pongs)
fmt.Println(<-pongs)
}A função ping só pode enviar para o channel, pong recebe de um e envia para outro. Isso documenta a intenção e previne erros de uso.
Boas práticas: Use channels unbuffered para sincronização e buffered para comunicação assíncrona controlada. Prefira definir direções nos parâmetros para clareza. Evite buffers grandes sem necessidade.
Referências
- Effective Go: Channels
- Go Specification: Channel types
- Go Blog: Go Concurrency Patterns: Pipelines and cancellation
- Go Wiki: Learn Concurrency
- Go Source: chan.go (runtime)
Exercícios
Crie um programa que use um channel unbuffered para enviar um inteiro de uma goroutine para a main. Imprima o valor recebido.
✓ Resposta:package main import "fmt" func main() { ch := make(chan int) go func() { ch <- 10 }() fmt.Println(<-ch) }Implemente dois workers que processam dados de um channel bufferizado. Crie um channel com buffer de 3, envie 3 números e faça dois workers receberem concorrentemente.
✓ Resposta:package main import ( "fmt" "sync" ) func worker(id int, jobs <-chan int, wg *sync.WaitGroup) { defer wg.Done() for j := range jobs { fmt.Printf("Worker %d processou job %d\n", id, j) } } func main() { jobs := make(chan int, 3) var wg sync.WaitGroup for w := 1; w <= 2; w++ { wg.Add(1) go worker(w, jobs, &wg) } for j := 1; j <= 3; j++ { jobs <- j } close(jobs) wg.Wait() }Escreva uma função que receba um channel de envio (chan<- string) e envie uma mensagem. Depois, crie um channel e chame a função.
✓ Resposta:package main import "fmt" func sendMessage(ch chan<- string, msg string) { ch <- msg } func main() { ch := make(chan string, 1) sendMessage(ch, "Olá, mundo!") fmt.Println(<-ch) }Crie um programa que use um channel de recebimento (<-chan bool) para esperar que uma goroutine termine. A goroutine deve dormir 1 segundo antes de enviar o sinal.
✓ Resposta:package main import ( "fmt" "time" ) func worker(done chan<- bool) { time.Sleep(1 * time.Second) done <- true } func main() { done := make(chan bool) go worker(done) <-done fmt.Println("Goroutine terminou") }Implemente um pipeline simples: uma goroutine gera números de 1 a 5 e envia para um channel; outra goroutine recebe e imprime o dobro. Use um channel bufferizado com buffer 2.
✓ Resposta:package main import "fmt" func generate(out chan<- int) { for i := 1; i <= 5; i++ { out <- i } close(out) } func double(in <-chan int, out chan<- int) { for v := range in { out <- v * 2 } close(out) } func main() { nums := make(chan int, 2) result := make(chan int, 2) go generate(nums) go double(nums, result) for r := range result { fmt.Println(r) } }