A instrução select
A instrução select em Go permite que uma goroutine aguarde múltiplas operações de canais simultaneamente, executando o caso que estiver pronto. Esta aula cobre multiplexação de canais, uso da cláusula default para evitar bloqueios, implementação de timeouts com time.After e exemplos práticos.
A instrução select é uma das ferramentas mais poderosas de concorrência em Go. Ela permite que uma goroutine espere e reaja a múltiplas operações de canais ao mesmo tempo, de forma semelhante a um switch, mas específica para canais. Com select, você pode implementar multiplexação, timeouts, operações não bloqueantes e muito mais.
Nesta aula, vamos explorar os conceitos fundamentais da instrução select, incluindo a multiplexação de canais, o uso da cláusula default para evitar bloqueios, a implementação de timeouts com time.After e exemplos práticos que consolidam o aprendizado.
Multiplexação
Multiplexação é a capacidade de lidar com múltiplos canais simultaneamente. Com select, você pode aguardar que qualquer um dos canais esteja pronto para comunicação (leitura ou escrita) e executar o bloco correspondente. Se mais de um canal estiver pronto, o Go escolhe um aleatoriamente.
Exemplo básico de multiplexação:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "mensagem do canal 1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "mensagem do canal 2"
}()
select {
case msg1 := <-ch1:
fmt.Println("Recebido:", msg1)
case msg2 := <-ch2:
fmt.Println("Recebido:", msg2)
}
// Pode imprimir "Recebido: mensagem do canal 1" ou "Recebido: mensagem do canal 2"
// dependendo de qual canal estiver pronto primeiro.
}Neste exemplo, duas goroutines enviam mensagens para canais diferentes após atrasos distintos. O select aguarda até que um dos canais esteja pronto e então executa o caso correspondente. Se ambos estiverem prontos ao mesmo tempo, a escolha é aleatória.
default
A cláusula default em um select é executada imediatamente se nenhum outro caso estiver pronto. Isso permite operações não bloqueantes: em vez de travar a goroutine esperando um canal, você pode fazer algo diferente.
Exemplo:
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
select {
case val := <-ch:
fmt.Println("Recebido:", val)
default:
fmt.Println("Nenhum dado disponível, continuando...")
}
}Neste código, como o canal ch não tem dados e não há ninguém enviando, o caso default é executado imediatamente, evitando que a goroutine principal bloqueie. Isso é útil para implementar loops de polling ou verificar canais sem travar.
Timeouts com time.After
Uma aplicação comum de select é implementar timeouts. A função time.After retorna um canal que recebe um valor após um determinado período. Combinando com select, podemos definir um prazo máximo para esperar uma operação em outro canal.
Exemplo:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
time.Sleep(3 * time.Second)
ch <- "resultado"
}()
select {
case res := <-ch:
fmt.Println("Recebido:", res)
case <-time.After(2 * time.Second):
fmt.Println("Timeout: a operação demorou demais")
}
}Aqui, a goroutine envia um resultado após 3 segundos, mas o select tem um timeout de 2 segundos. Como o timeout expira primeiro, o programa imprime "Timeout: a operação demorou demais". Se o canal recebesse o resultado antes de 2 segundos, o primeiro caso seria executado.
Exemplos
Vamos a alguns exemplos mais elaborados que combinam os conceitos.
Exemplo 1: Fan-in (multiplexação de múltiplos canais)
package main
import (
"fmt"
"time"
)
func producer(ch chan<- int, start int, interval time.Duration) {
for i := start; ; i++ {
ch <- i
time.Sleep(interval)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go producer(ch1, 1, 500*time.Millisecond)
go producer(ch2, 100, 800*time.Millisecond)
for i := 0; i < 10; i++ {
select {
case val := <-ch1:
fmt.Println("ch1:", val)
case val := <-ch2:
fmt.Println("ch2:", val)
}
}
}Este exemplo cria dois produtores que enviam números inteiros para canais diferentes com intervalos distintos. O select no loop principal multiplexa a recepção, imprimindo os valores à medida que chegam.
Exemplo 2: Operação não bloqueante com default
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 1)
ch <- 10
select {
case val := <-ch:
fmt.Println("Recebido:", val)
default:
fmt.Println("Canal vazio")
}
select {
case ch <- 20:
fmt.Println("Enviado 20")
default:
fmt.Println("Canal cheio")
}
}Neste exemplo, usamos default para verificar se é possível ler ou escrever em um canal sem bloquear. Primeiro lemos um valor (sucesso), depois tentamos escrever outro (falha porque o canal tem buffer de 1 e já está cheio).
Exemplo 3: Timeout em loop
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
time.Sleep(1 * time.Second)
ch <- i
}
}()
for {
select {
case val := <-ch:
fmt.Println("Recebido:", val)
case <-time.After(3 * time.Second):
fmt.Println("Timeout global, saindo...")
return
}
}
}Aqui, o loop aguarda valores do canal, mas se nenhum valor chegar em 3 segundos, o timeout é acionado e o programa termina. Note que time.After cria um novo canal a cada iteração, então o timeout é resetado a cada ciclo.
Boas práticas e observações finais
- Use
selectcomdefaultpara operações não bloqueantes, mas evite polling excessivo que pode consumir CPU. - Timeouts com
time.Aftersão simples, mas lembre-se de que cada chamada cria um novo canal; em loops, isso pode gerar garbage collection. Para maior eficiência, reutilize um único timer comtime.NewTimer. - Em multiplexação, se vários canais estiverem prontos, a escolha é aleatória; não assuma ordem.
- Evite
selectvazio (sem casos) em goroutines, pois isso bloqueia para sempre (equivalente a umfor {}). - Use
selectpara implementar padrões como fan-in, fan-out, orquestração de goroutines e cancelamento com canais.
Referências
- Go Language Specification: Select statements
- Effective Go: Select
- Go Concurrency Patterns (Rob Pike)
- Package time: After
- Go by Example: Select
Exercícios
Crie um programa que use
selectpara ler de dois canais: um que recebe números ímpares e outro que recebe números pares (ambos gerados por goroutines separadas). Imprima os valores à medida que chegam, até que 10 valores tenham sido lidos no total.✓ Resposta:package main import ( "fmt" ) func main() { oddCh := make(chan int) evenCh := make(chan int) go func() { for i := 1; ; i += 2 { oddCh <- i } }() go func() { for i := 2; ; i += 2 { evenCh <- i } }() for i := 0; i < 10; i++ { select { case val := <-oddCh: fmt.Println("Ímpar:", val) case val := <-evenCh: fmt.Println("Par:", val) } } }Escreva um programa que tente enviar um valor para um canal com buffer de tamanho 1. Se o canal estiver cheio, imprima "Canal cheio" e prossiga sem bloquear. Use
selectcomdefault.✓ Resposta:package main import "fmt" func main() { ch := make(chan int, 1) ch <- 10 select { case ch <- 20: fmt.Println("Enviado 20") default: fmt.Println("Canal cheio") } }Implemente uma função que receba um canal de inteiros e um timeout (em segundos). A função deve tentar ler do canal e retornar o valor, mas se o timeout expirar, deve retornar um erro. Use
time.Aftereselect.✓ Resposta:package main import ( "errors" "fmt" "time" ) func readWithTimeout(ch <-chan int, timeoutSeconds int) (int, error) { select { case val := <-ch: return val, nil case <-time.After(time.Duration(timeoutSeconds) * time.Second): return 0, errors.New("timeout") } } func main() { ch := make(chan int) go func() { time.Sleep(2 * time.Second) ch <- 42 }() val, err := readWithTimeout(ch, 1) if err != nil { fmt.Println(err) } else { fmt.Println(val) } }Crie um programa que use
selectpara receber dados de três canais diferentes. Cada canal deve ser alimentado por uma goroutine que envia uma string diferente após um atraso aleatório (entre 0 e 3 segundos). O programa deve imprimir as mensagens na ordem em que chegam, até receber três mensagens.✓ Resposta:package main import ( "fmt" "math/rand" "time" ) func main() { ch1 := make(chan string) ch2 := make(chan string) ch3 := make(chan string) rand.Seed(time.Now().UnixNano()) go func() { time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond) ch1 <- "um" }() go func() { time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond) ch2 <- "dois" }() go func() { time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond) ch3 <- "três" }() for i := 0; i < 3; i++ { select { case msg := <-ch1: fmt.Println("Recebido:", msg) case msg := <-ch2: fmt.Println("Recebido:", msg) case msg := <-ch3: fmt.Println("Recebido:", msg) } } }Escreva um programa que simule um servidor que aceita requisições de dois tipos (por exemplo, "ler" e "escrever") em canais separados. Use
selectpara processar as requisições à medida que chegam, imprimindo o tipo e um identificador. Para simplificar, gere 5 requisições de cada tipo em goroutines com intervalos aleatórios.✓ Resposta:package main import ( "fmt" "math/rand" "time" ) type Request struct { Type string ID int } func main() { readCh := make(chan Request) writeCh := make(chan Request) rand.Seed(time.Now().UnixNano()) // Gerar requisições de leitura go func() { for i := 1; i <= 5; i++ { time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) readCh <- Request{Type: "ler", ID: i} } }() // Gerar requisições de escrita go func() { for i := 1; i <= 5; i++ { time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) writeCh <- Request{Type: "escrever", ID: i} } }() for i := 0; i < 10; i++ { select { case req := <-readCh: fmt.Printf("Processando requisição de %s #%d\n", req.Type, req.ID) case req := <-writeCh: fmt.Printf("Processando requisição de %s #%d\n", req.Type, req.ID) } } }