A instrução select
A instrução select em Go permite que uma goroutine aguarde múltiplas operações de canais simultaneamente, funcionando como um switch para canais. Esta aula cobre multiplexação de canais, uso da cláusula default para operações não bloqueantes, implementação de timeouts com time.After e exemplos práticos de cada conceito.
A instrução select é uma das ferramentas mais poderosas para concorrência em Go. Ela permite que uma goroutine aguarde e opere em múltiplos canais ao mesmo tempo, de forma similar a um switch, mas com casos baseados em operações de comunicação (envio ou recebimento em canais). O select bloqueia a execução até que um de seus casos possa ser executado, e então executa aquele caso. Se múltiplos casos estiverem prontos, um é escolhido aleatoriamente.
Nesta aula, exploraremos como multiplexar canais com select, como usar a cláusula default para evitar bloqueios, como implementar timeouts com time.After, e veremos exemplos que consolidam esses conceitos.
Multiplexação
Multiplexação é a capacidade de lidar com múltiplos canais em uma única goroutine, reagindo ao primeiro canal que estiver pronto. O select permite isso de forma elegante. Cada caso em um select é uma operação de canal (envio ou recebimento). O select aguarda até que um dos casos possa prosseguir e então executa o bloco correspondente.
Por exemplo, suponha que temos duas goroutines que produzem dados em canais diferentes. Podemos usar select para consumir os dados à medida que chegam, sem ordem fixa.
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "um"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "dois"
}()
select {
case msg1 := <-ch1:
fmt.Println("Recebido de ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("Recebido de ch2:", msg2)
}
}
Neste exemplo, o select aguarda até que um dos canais tenha um valor disponível. Como ch1 demora 1 segundo e ch2 demora 2 segundos, o primeiro caso será executado após 1 segundo. Se ambos estivessem prontos ao mesmo tempo, um seria escolhido aleatoriamente.
default
A cláusula default em um select é executada imediatamente se nenhum outro caso estiver pronto. Isso permite criar operações não bloqueantes em canais. Sem default, o select bloqueia até que algum caso possa ser executado. Com default, ele se torna um select não bloqueante.
O default é útil para tentar enviar ou receber sem travar a execução, permitindo que a goroutine faça outro trabalho enquanto aguarda.
package main
import "fmt"
func main() {
ch := make(chan int, 1)
ch <- 42
select {
case val := <-ch:
fmt.Println("Recebido:", val)
default:
fmt.Println("Canal vazio")
}
select {
case ch <- 100:
fmt.Println("Enviado 100")
default:
fmt.Println("Canal cheio")
}
}
No primeiro select, o canal tem um valor (42), então o caso de recebimento é executado e o default é ignorado. No segundo, o canal tem buffer de tamanho 1 e já está cheio (com 42), então o envio falha e o default é executado.
Timeouts com time.After
Uma aplicação comum do select é implementar timeouts. A função time.After retorna um canal que recebe um valor após um determinado período. Combinando isso com select, podemos fazer com que uma operação de canal desista após um tempo limite.
Por exemplo, se estamos aguardando uma resposta de uma goroutine, mas não queremos esperar indefinidamente, usamos time.After para definir um prazo.
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!")
}
}
Aqui, a goroutine demora 3 segundos para enviar, mas o timeout é de 2 segundos. Portanto, o caso time.After será executado primeiro, imprimindo "Timeout!". Se o tempo de espera da goroutine fosse menor que 2 segundos, o primeiro caso seria executado.
Exemplos
Vamos consolidar os conceitos com um exemplo mais completo que combina multiplexação, default e timeout.
Considere um cenário onde temos duas fontes de dados (canais) e queremos processar a primeira que chegar, mas com um timeout geral. Além disso, se nenhum dado estiver disponível imediatamente, queremos fazer uma ação alternativa (default).
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
// Goroutine que envia após 1 segundo
go func() {
time.Sleep(1 * time.Second)
ch1 <- "dado rápido"
}()
// Goroutine que envia após 3 segundos
go func() {
time.Sleep(3 * time.Second)
ch2 <- "dado lento"
}()
select {
case msg := <-ch1:
fmt.Println("Recebido de ch1:", msg)
case msg := <-ch2:
fmt.Println("Recebido de ch2:", msg)
case <-time.After(2 * time.Second):
fmt.Println("Timeout após 2 segundos")
default:
fmt.Println("Nenhum canal pronto, fazendo outra coisa")
}
}
Neste exemplo, como as goroutines têm delays, a execução pode variar. Se executarmos rapidamente, o default será executado porque nenhum canal está pronto imediatamente. Se esperarmos 1 segundo, o caso de ch1 será executado (antes do timeout de 2s). Se esperarmos mais de 2s, o timeout ocorre. Se ambos os canais estivessem prontos ao mesmo tempo, um seria escolhido aleatoriamente.
Outro exemplo útil é um loop que lê de múltiplos canais até que uma condição de parada seja sinalizada.
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
quit := make(chan bool)
// Goroutine que envia números a cada 500ms
go func() {
for i := 0; i < 5; i++ {
time.Sleep(500 * time.Millisecond)
ch <- i
}
close(ch)
}()
// Goroutine que sinaliza para parar após 2 segundos
go func() {
time.Sleep(2 * time.Second)
quit <- true
}()
for {
select {
case val, ok := <-ch:
if !ok {
fmt.Println("Canal fechado, saindo")
return
}
fmt.Println("Recebido:", val)
case <-quit:
fmt.Println("Sinal de quit recebido, saindo")
return
}
}
}
Este exemplo mostra um padrão comum de uso de select em loops para processar dados até que uma condição de término ocorra.
Boas práticas e observações finais
O select é uma instrução síncrona: ele bloqueia até que um caso possa executar (a menos que tenha default). Lembre-se de que canais nil nunca estão prontos, então um caso com canal nil é ignorado. Isso pode ser usado para desabilitar dinamicamente casos em um select.
Sempre que possível, evite usar default em loops apertados sem pausa, pois pode consumir CPU desnecessariamente. Prefira usar time.Sleep ou outras estratégias se precisar de polling.
O select é frequentemente usado em conjunto com for para criar loops de evento, como em servidores ou workers concorrentes.
Referências
- Go Specification: Select statements
- A Tour of Go: Select
- Go by Example: Select
- Package time: After
- Go Blog: Go Concurrency Patterns: Pipelines and cancellation
Exercícios
-
Escreva um programa que use
selectpara receber de dois canais de string e imprima a primeira mensagem recebida. Crie duas goroutines que enviem mensagens após 1 segundo e 2 segundos, respectivamente.✓ Resposta:package main import ( "fmt" "time" ) func main() { ch1 := make(chan string) ch2 := make(chan string) go func() { time.Sleep(1 * time.Second) ch1 <- "mensagem 1" }() go func() { time.Sleep(2 * time.Second) ch2 <- "mensagem 2" }() select { case msg := <-ch1: fmt.Println(msg) case msg := <-ch2: fmt.Println(msg) } } -
Modifique o programa anterior para incluir uma cláusula
defaultque imprima "Nenhuma mensagem pronta" se nenhum canal estiver pronto imediatamente.✓ Resposta:package main import ( "fmt" "time" ) func main() { ch1 := make(chan string) ch2 := make(chan string) go func() { time.Sleep(1 * time.Second) ch1 <- "mensagem 1" }() go func() { time.Sleep(2 * time.Second) ch2 <- "mensagem 2" }() select { case msg := <-ch1: fmt.Println(msg) case msg := <-ch2: fmt.Println(msg) default: fmt.Println("Nenhuma mensagem pronta") } } -
Crie um programa que use
selectcomtime.Afterpara implementar um timeout de 3 segundos ao aguardar um valor de um canal que nunca recebe dados (simule uma goroutine que nunca envia).✓ Resposta:package main import ( "fmt" "time" ) func main() { ch := make(chan string) // goroutine que nunca envia go func() { // não faz nada }() select { case msg := <-ch: fmt.Println("Recebido:", msg) case <-time.After(3 * time.Second): fmt.Println("Timeout!") } } -
Escreva um programa que utilize um loop com
selectpara ler de um canal de inteiros até que o canal seja fechado. O canal deve receber 3 números de uma goroutine (com intervalos de 500ms) e depois ser fechado.✓ Resposta:package main import ( "fmt" "time" ) func main() { ch := make(chan int) go func() { for i := 1; i <= 3; i++ { time.Sleep(500 * time.Millisecond) ch <- i } close(ch) }() for { select { case val, ok := <-ch: if !ok { fmt.Println("Canal fechado") return } fmt.Println("Recebido:", val) } } } -
Crie um programa que combine multiplexação, default e timeout: duas goroutines enviam mensagens em tempos diferentes (1s e 4s), e o programa deve usar
selectpara imprimir a primeira mensagem recebida, mas se nenhuma chegar em 2 segundos, imprimir "Timeout". Se ambas estiverem prontas imediatamente (não é o caso aqui), o default não deve ser executado.✓ Resposta:package main import ( "fmt" "time" ) func main() { ch1 := make(chan string) ch2 := make(chan string) go func() { time.Sleep(1 * time.Second) ch1 <- "rápida" }() go func() { time.Sleep(4 * time.Second) ch2 <- "lenta" }() select { case msg := <-ch1: fmt.Println("Recebido de ch1:", msg) case msg := <-ch2: fmt.Println("Recebido de ch2:", msg) case <-time.After(2 * time.Second): fmt.Println("Timeout") default: fmt.Println("Nenhum canal pronto") } }