Select e Padrões de Concorrência
Lição 15: Select e Padrões de Concorrência
Analogia
Imagine que você é um garçom de restaurante responsável simultaneamente por três mesas:
- O cliente da Mesa A está fazendo o pedido
- O cliente da Mesa B está pagando a conta
- O cliente da Mesa C está pedindo água
Você não esperaria tolasmente por apenas uma mesa — em vez disso, você observa todas as mesas ao mesmo tempo e atende quem levantar a mão primeiro. É assim que o select funciona — ele escuta múltiplos channels simultaneamente e executa aquele que estiver pronto primeiro.
Conceitos Fundamentais
| Conceito | Descrição |
|---|---|
select |
Uma instrução que escuta múltiplas operações de channel simultaneamente |
case |
Cada operação de channel corresponde a um分支 |
default |
Executa quando nenhum channel está pronto (opcional |
| Comportamento bloqueante | Sem default, select bloqueia até que um case esteja pronto |
| Seleção aleatória | Quando múltiplos cases estão prontos simultaneamente, um é escolhido aleatoriamente |
Sintaxe Básica e Uso
Instrução select Padrão
select {
case msg := <-ch1:
// Dados recebidos de ch1
fmt.Println(msg)
case ch2 <- "olá":
// Dados enviados com sucesso para ch2
case <-ch3:
// ch3 está fechado ou dados recebidos
default:
// Executa quando nenhum channel está pronto
}
Controle de Timeout (Padrão Mais Comum)
select {
case msg := <-ch:
fmt.Println("Recebido:", msg)
case <-time.After(3 * time.Second):
fmt.Println("Tempo esgotado!")
}
Operação de Channel Não Bloqueante
select {
case msg := <-ch:
fmt.Println("Recebido:", msg)
default:
fmt.Println("Nenhum dado disponível, retornando imediatamente")
}
selectdeve ter pelo menos umcase; não pode ser totalmente vazio- Sem
defaulte com todos oscases não prontos,selectvai bloquear permanentemente time.After()retorna um channel que recebe um valor após a duração especificada- Quando múltiplos
cases estão prontos simultaneamente, Go escolhe um aleatoriamente para prevenir inanição
Exemplo: Uso Básico de Select (Dificuldade ⭐)
Demonstra como escutar dois channels simultaneamente:
package main
import (
"fmt"
"time"
)
// Simula duas fontes de dados
func source(name string, ch chan<- string, delay time.Duration) {
for i := 1; i <= 3; i++ {
time.Sleep(delay)
ch <- fmt.Sprintf("[%s] Mensagem %d", name, i)
}
close(ch)
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go source("FonteA", ch1, 500*time.Millisecond)
go source("FonteB", ch2, 800*time.Millisecond)
// Usa select para escutar ambos os channels simultaneamente
// Precisa escutar 6 vezes (3 mensagens de cada fonte)
for i := 0; i < 6; i++ {
select {
case msg, ok := <-ch1:
if ok {
fmt.Println("De ch1:", msg)
}
case msg, ok := <-ch2:
if ok {
fmt.Println("De ch2:", msg)
}
}
}
fmt.Println("Todas as mensagens processadas")
}
go run main.go
De ch1: [FonteA] Mensagem 1
De ch2: [FonteB] Mensagem 1
De ch1: [FonteA] Mensagem 2
De ch1: [FonteA] Mensagem 3
De ch2: [FonteB] Mensagem 2
De ch2: [FonteB] Mensagem 3
Todas as mensagens processadas
Exemplo: Controle de Timeout e Channel de Saída (Dificuldade ⭐⭐)
Use select para implementar um worker com timeout e desligamento gracioso:
package main
import (
"fmt"
"math/rand"
"time"
)
// worker simula uma tarefa demorada
func worker(id int, jobs <-chan int, results chan<- string, done chan<- int) {
for job := range jobs {
// Simula tempo de processamento imprevisível
duration := time.Duration(rand.Intn(500)+200) * time.Millisecond
time.Sleep(duration)
results <- fmt.Sprintf("Worker %d completou tarefa %d (levou %v)", id, job, duration)
}
done <- id
}
func main() {
jobs := make(chan int, 10)
results := make(chan string, 10)
done := make(chan int, 3)
// Inicia 3 workers
for i := 1; i <= 3; i++ {
go worker(i, jobs, results, done)
}
// Distribui 6 tarefas
for j := 1; j <= 6; j++ {
jobs <- j
}
close(jobs)
// Coleta resultados com controle de timeout
timeout := time.After(2 * time.Second)
finished := 0
for finished < 6 {
select {
case result := <-results:
fmt.Println(result)
finished++
case workerID := <-done:
fmt.Printf(">>> Worker %d saiu\n", workerID)
case <-timeout:
fmt.Println("⏰ Timeout! Algumas tarefas incompletas")
return
}
}
fmt.Println("Todas as tarefas concluídas")
}
go run main.go
Worker 2 completou tarefa 2 (levou 234ms)
Worker 1 completou tarefa 1 (levou 345ms)
Worker 3 completou tarefa 3 (levou 289ms)
Worker 2 completou tarefa 4 (levou 412ms)
Worker 1 completou tarefa 5 (levou 198ms)
>>> Worker 1 saiu
Worker 3 completou tarefa 6 (levou 367ms)
Todas as tarefas concluídas
Exemplo: Fan-in / Fan-out e Padrão de Pipeline (Dificuldade ⭐⭐⭐)
Demonstra padrões clássicos de concorrência: Pipeline + Fan-out + Fan-in:
package main
import (
"fmt"
"sync"
"time"
)
// Estágio 1: Gerador de dados (início do Pipeline)
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
// Estágio 2: Computação de quadrados (estágio intermediário do Pipeline)
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
time.Sleep(100 * time.Millisecond) // simula tempo de computação
out <- n * n
}
close(out)
}()
return out
}
// Fan-out: distribui um channel para múltiplos workers
func fanOut(in <-chan int, workers int) []<-chan int {
channels := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
channels[i] = square(in)
}
return channels
}
// Fan-in: mescla múltiplos channels em um
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)
// Inicia uma goroutine para cada channel para encaminhar dados
wg.Add(len(channels))
for _, ch := range channels {
go func(c <-chan int) {
defer wg.Done()
for val := range c {
out <- val
}
}(ch)
}
// Fecha o channel de saída após todos os channels de entrada serem fechados
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
// Estágio 1: Gera dados
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
source := generator(nums...)
// Estágio 2: Fan-out para 3 workers para computação paralela
workers := fanOut(source, 3)
// Estágio 3: Fan-in para mesclar resultados
merged := fanIn(workers...)
// Coleta todos os resultados
results := make(map[int]bool)
for val := range merged {
results[val] = true
fmt.Printf("Resultado recebido: %d\n", val)
}
fmt.Println("\nResultados deduplicados:")
for val := range results {
fmt.Printf(" %d\n", val)
}
}
go run main.go
Resultado recebido: 1
Resultado recebido: 4
Resultado recebido: 9
Resultado recebido: 16
Resultado recebido: 25
Resultado recebido: 36
Resultado recebido: 49
Resultado recebido: 64
Resultado recebido: 81
Resultado recebido: 100
Resultados deduplicados:
1
4
9
16
25
36
49
64
81
100
Cenário 1: Disputa de Requisição HTTP
Envia requisições para múltiplos servidores simultaneamente, usando a primeira resposta:
package main
import (
"fmt"
"time"
)
// Simula envio de requisições para diferentes servidores
func fetchFromServer(name string, delay time.Duration) <-chan string {
ch := make(chan string, 1)
go func() {
time.Sleep(delay)
ch <- fmt.Sprintf("Resposta de %s", name)
}()
return ch
}
// Disputa de requisições: retorna a resposta mais rápida
func race(urls map[string]time.Duration, timeout time.Duration) (string, error) {
// Cria um channel para cada servidor
ch := make(chan string, len(urls))
for name, delay := range urls {
go func(n string, d time.Duration) {
ch <- fetchResult(n, d)
}(name, delay)
}
// Espera pela primeira resposta ou timeout
select {
case result := <-ch:
return result, nil
case <-time.After(timeout):
return "", fmt.Errorf("todos os servidores excederam o tempo")
}
}
func fetchResult(name string, delay time.Duration) string {
time.Sleep(delay)
return fmt.Sprintf("Dados de %s (latência %v)", name, delay)
}
func main() {
servers := map[string]time.Duration{
"ServidorA-Beijing": 800 * time.Millisecond,
"ServidorB-Shanghai": 300 * time.Millisecond,
"ServidorC-Guangzhou": 500 * time.Millisecond,
}
result, err := race(servers, 2*time.Second)
if err != nil {
fmt.Println("Erro:", err)
} else {
fmt.Println("Resultado da disputa:", result)
}
}
go run main.go
Resultado da disputa: Dados de ServidorB-Shanghai (latência 300ms)
Cenário 2: Desligamento Gracioso
Use um channel quit para implementar o desligamento gracioso de um serviço:
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
// server simula um serviço em execução contínua
func server(id int, quit <-chan struct{}) {
fmt.Printf("[Serviço %d] Iniciado\n", id)
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Printf("[Serviço %d] Processando requisição...\n", id)
case <-quit:
fmt.Printf("[Serviço %d] Sinal de saída recebido, limpando...", id)
time.Sleep(200 * time.Millisecond) // simula limpeza
fmt.Printf("[Serviço %d] Parado\n", id)
return
}
}
}
func main() {
// Escuta sinais de interrupção do sistema (Ctrl+C)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Cria channel de saída
quit := make(chan struct{})
// Inicia múltiplos serviços
for i := 1; i <= 3; i++ {
go server(i, quit)
}
fmt.Println("Programa principal rodando, pressione Ctrl+C para saída graciosa...")
// Espera sinal do sistema
sig := <-sigChan
fmt.Printf("\nSinal recebido: %v, iniciando saída graciosa...\n", sig)
// Notifica todos os serviços para sair
close(quit)
// Dá tempo aos serviços para completar a limpeza
time.Sleep(1 * time.Second)
fmt.Println("Todos os serviços fechados, programa encerrando")
}
go run main.go
# Pressione Ctrl+C para acionar saída graciosa
Programa principal rodando, pressione Ctrl+C para saída graciosa...
[Serviço 1] Iniciado
[Serviço 2] Iniciado
[Serviço 3] Iniciado
[Serviço 1] Processando requisição...
[Serviço 2] Processando requisição...
[Serviço 3] Processando requisição...
[Serviço 1] Processando requisição...
^C
Sinal recebido: interrupt, iniciando saída graciosa...
[Serviço 1] Sinal de saída recebido, limpando...[Serviço 1] Parado
[Serviço 2] Sinal de saída recebido, limpando...[Serviço 2] Parado
[Serviço 3] Sinal de saída recebido, limpando...[Serviço 3] Parado
Todos os serviços fechados, programa encerrando
❓ Perguntas Frequentes
P1: O que acontece quando múltiplos cases em select estão prontos simultaneamente?
Go seleciona aleatoriamente um para executar, em vez de pegar o primeiro em ordem. Isso garante justiça e previne que qualquer channel sofra inanição.
ch1 := make(chan string, 1)
ch2 := make(chan string, 1)
ch1 <- "A"
ch2 <- "B"
// O resultado é aleatório, pode ser A ou B
select {
case v := <-ch1:
fmt.Println(v)
case v := <-ch2:
fmt.Println(v)
}
P2: Quando o branch default em select deve ser usado?
Use default quando você precisa de operações não bloqueantes. Sem default, select bloqueia até que um case esteja pronto.
// Recebimento não bloqueante
select {
case msg := <-ch:
fmt.Println("Recebido:", msg)
default:
fmt.Println("Nenhum dado no channel no momento")
}
P3: Como implementar um loop infinito escutando múltiplos channels?
Use o padrão de combinação for-select:
for {
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
case <-quit:
fmt.Println("Saindo do loop")
return
}
}
P4: time.After em um loop causa vazamento de memória?
Sim! Cada iteração do loop com time.After cria um novo timer que não será coletado pelo garbage collector. A abordagem correta é usar time.NewTimer:
// ❌ Errado: cria um novo timer a cada iteração
for {
select {
case msg := <-ch:
fmt.Println(msg)
case <-time.After(5 * time.Second):
fmt.Println("Tempo esgotado")
}
}
// ✅ Correto: reutiliza o timer
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
for {
select {
case msg := <-ch:
fmt.Println(msg)
if !timer.Stop() {
<-timer.C
}
timer.Reset(5 * time.Second)
case <-timer.C:
fmt.Println("Tempo esgotado")
return
}
}
📖 Resumo
| Padrão | Propósito | Código-chave |
|---|---|---|
| Controle de timeout | Limita tempo de espera da operação | case <-time.After(d) |
| Operação não bloqueante | Retorna imediatamente sem esperar | select com default |
| Saída graciosa | Escuta channel de saída | case <-quit |
| Fan-out | Uma entrada distribuída para múltiplos workers | Múltiplas goroutines lendo de um channel |
| Fan-in | Múltiplas entradas mescladas em um channel | sync.WaitGroup + encaminhamento |
| Pipeline | Processamento em múltiplos estágios | Channels encadeados |
Principais Conclusões:
selecté a ferramenta central de concorrência do Go para multiplexação- Quando múltiplos cases estão prontos simultaneamente, um é escolhido aleatoriamente para justiça
for-selecté o padrão padrão para escutar múltiplos channels- Cuidado com vazamentos de memória de
time.Afterem loops - Fan-in/Fan-out/Pipeline são padrões clássicos para construir pipelines concorrentes
📝 Exercícios
Exercício 1: Timer de Contagem Regressiva
Crie um programa usando select e time.Ticker para implementar uma contagem regressiva de 5 segundos, imprimindo o tempo restante a cada segundo, e imprimindo "Lançamento!" no 0.
// Dicas:
// ticker := time.NewTicker(1 * time.Second)
// select {
// case <-ticker.C:
// // atualiza contagem regressiva
// }
Exercício 2: Merge Sort Multi-caminho
Implemente uma função que recebe múltiplos channels de inteiros ordenados e usa o padrão Fan-in para mesclá-los em um único channel de saída ordenado.
// Assinatura da função:
func mergeSorted(channels ...<-chan int) <-chan int {
// Implementa lógica de merge
}
Exercício 3: Pipeline com Cancelamento
Construa um Pipeline de três estágios (gerar → filtrar pares → multiplicar por 10) que suporte cancelamento de todo o pipeline via context:
// Dicas:
// ctx, cancel := context.WithCancel(context.Background())
// Verifique ctx.Done() no select de cada estágio
Próxima Lição: Lição 16: Sync e Segurança de Concorrência — Aprenda sobre sync.Mutex, sync.WaitGroup, sync.Once e outras primitivas de sincronização para acesso seguro a recursos compartilhados.



