Select: multiplexando channels
1. Introdução ao select e multiplexação de canais
Em Go, o select é uma estrutura de controle que permite que uma goroutine aguarde múltiplas operações de comunicação em canais simultaneamente. Funciona de forma análoga ao switch, mas especificamente para canais: cada case representa uma operação de envio ou recebimento em um canal. O select bloqueia até que um dos casos possa ser executado.
A multiplexação de canais é a capacidade de escutar vários canais ao mesmo tempo e reagir ao primeiro que estiver pronto. Isso é fundamental para construir sistemas concorrentes responsivos e eficientes.
Exemplo básico:
package main
import (
"fmt"
"time"
)
func main() {
canal1 := make(chan string)
canal2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
canal1 <- "mensagem do canal 1"
}()
go func() {
time.Sleep(2 * time.Second)
canal2 <- "mensagem do canal 2"
}()
select {
case msg1 := <-canal1:
fmt.Println("Recebido:", msg1)
case msg2 := <-canal2:
fmt.Println("Recebido:", msg2)
}
}
Neste exemplo, o select aguarda até que uma das goroutines envie dados para seu respectivo canal. Como o canal1 recebe dados primeiro (após 1 segundo), a saída será "Recebido: mensagem do canal 1".
2. Sintaxe e funcionamento do select
A estrutura básica do select é:
select {
case <-canalA:
// executado quando canalA tem dados
case canalB <- valor:
// executado quando pudermos enviar para canalB
default:
// executado se nenhum caso estiver pronto
}
Uma característica importante é a seleção aleatória quando múltiplos casos estão prontos simultaneamente. Go implementa fairness (justiça) escolhendo aleatoriamente entre os casos prontos, evitando starvation.
package main
import (
"fmt"
"time"
)
func main() {
canal := make(chan string, 1)
canal <- "dado"
select {
case msg := <-canal:
fmt.Println("Caso 1:", msg)
case msg := <-canal:
fmt.Println("Caso 2:", msg)
default:
fmt.Println("Nenhum caso pronto")
}
}
Apesar de termos apenas um dado no canal, o select só executará um caso (aleatoriamente, se houvesse múltiplos canais prontos).
3. Tratamento de canais nulos e o caso default
Canais nil em um select são ignorados — nunca são selecionados. Isso pode ser usado para desativar dinamicamente um caso.
O default permite operações não bloqueantes:
package main
import (
"fmt"
"time"
)
func main() {
canal := make(chan int, 1)
// Tentativa de leitura não bloqueante
select {
case val := <-canal:
fmt.Println("Valor recebido:", val)
default:
fmt.Println("Canal vazio, continuando...")
}
// Envio não bloqueante
select {
case canal <- 42:
fmt.Println("Valor enviado com sucesso")
default:
fmt.Println("Canal cheio, descartando valor")
}
}
4. Timeouts com select e time.After
Timeouts são implementados elegantemente com time.After:
package main
import (
"fmt"
"time"
)
func leituraComTimeout(canal <-chan string, timeout time.Duration) (string, error) {
select {
case msg := <-canal:
return msg, nil
case <-time.After(timeout):
return "", fmt.Errorf("timeout após %v", timeout)
}
}
func main() {
canal := make(chan string)
go func() {
time.Sleep(3 * time.Second)
canal <- "dado atrasado"
}()
resultado, err := leituraComTimeout(canal, 2*time.Second)
if err != nil {
fmt.Println("Erro:", err)
} else {
fmt.Println("Resultado:", resultado)
}
}
Cuidado: Em loops, time.After cria um novo timer a cada iteração, que pode vazar memória se o loop for muito longo. Prefira time.NewTimer e reutilize-o.
5. select vazio e loops infinitos
Um select {} bloqueia para sempre, pois não há casos para selecionar. É útil para manter uma goroutine principal viva:
package main
import (
"fmt"
"time"
)
func trabalhador(id int, tarefas <-chan string) {
for {
select {
case tarefa, ok := <-tarefas:
if !ok {
fmt.Printf("Trabalhador %d encerrando\n", id)
return
}
fmt.Printf("Trabalhador %d processando: %s\n", id, tarefa)
}
}
}
func main() {
tarefas := make(chan string, 3)
tarefas <- "tarefa 1"
tarefas <- "tarefa 2"
close(tarefas)
go trabalhador(1, tarefas)
time.Sleep(100 * time.Millisecond)
// select {} // manteria o programa rodando indefinidamente
}
6. Cancelamento com select e done channel
O padrão done channel é essencial para encerramento limpo de goroutines:
package main
import (
"fmt"
"time"
)
func worker(done <-chan struct{}, tarefas <-chan int) {
for {
select {
case tarefa := <-tarefas:
fmt.Printf("Processando tarefa %d\n", tarefa)
time.Sleep(100 * time.Millisecond)
case <-done:
fmt.Println("Worker recebeu sinal de parada")
return
}
}
}
func main() {
tarefas := make(chan int, 5)
done := make(chan struct{})
go worker(done, tarefas)
for i := 1; i <= 3; i++ {
tarefas <- i
}
close(done) // sinaliza parada
time.Sleep(200 * time.Millisecond)
fmt.Println("Programa finalizado")
}
7. Padrão de tentativa (try-send / try-receive)
Envios e recebimentos não bloqueantes são úteis para buffers cheios ou descarte de mensagens antigas:
package main
import (
"fmt"
"time"
)
func main() {
buffer := make(chan string, 2)
// Try-send: envia apenas se houver espaço
for i := 0; i < 5; i++ {
select {
case buffer <- fmt.Sprintf("msg %d", i):
fmt.Printf("Mensagem %d enviada\n", i)
default:
fmt.Printf("Buffer cheio, mensagem %d descartada\n", i)
}
}
// Try-receive: recebe apenas se houver dados
for i := 0; i < 5; i++ {
select {
case msg := <-buffer:
fmt.Println("Lido:", msg)
default:
fmt.Println("Buffer vazio")
}
}
}
8. Boas práticas e armadilhas comuns
Evite select sem controle de saída em loops:
// RUIM: loop infinito sem condição de saída
for {
select {
case msg := <-canal:
processar(msg)
}
}
// BOM: com done channel
for {
select {
case msg := <-canal:
processar(msg)
case <-done:
return
}
}
Cuidado com time.After em loops longos:
// RUIM: vazamento de memória
for {
select {
case msg := <-canal:
processar(msg)
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}
}
// BOM: reutilizando timer
timer := time.NewTimer(1 * time.Second)
defer timer.Stop()
for {
timer.Reset(1 * time.Second)
select {
case msg := <-canal:
processar(msg)
case <-timer.C:
fmt.Println("timeout")
}
}
Prefira select com done channel para encerramento limpo: Sempre forneça uma maneira de encerrar goroutines graciosamente, evitando deadlocks e vazamentos.
Referências
- Select statement (The Go Programming Language Specification) — Documentação oficial da sintaxe e semântica do select em Go
- Go by Example: Select — Exemplos práticos e didáticos de uso do select
- Concurrency in Go: Tools and Techniques — Livro que aborda padrões avançados com select e canais
- Using Go's select for timeouts and non-blocking communication — Tutorial detalhado sobre timeouts e comunicação não bloqueante com select
- The Go Blog: Go Concurrency Patterns: Timing out, moving on — Artigo oficial sobre padrões de timeout com select
- Golang Select Statement: Complete Guide — Guia completo com exemplos de multiplexação de canais
- Effective Go: Channels — Documentação oficial com boas práticas para uso de canais e select