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:

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

GO
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)

GO
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

GO
select {
case msg := <-ch:
    fmt.Println("Recebido:", msg)
default:
    fmt.Println("Nenhum dado disponível, retornando imediatamente")
}
💡 Dica:

  • select deve ter pelo menos um case; não pode ser totalmente vazio
  • Sem default e com todos os cases não prontos, select vai 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:

GO
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")
}
▶ Experimente
BASH
go run main.go
TEXT
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:

GO
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")
}
▶ Experimente
BASH
go run main.go
TEXT
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:

GO
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)
	}
}
▶ Experimente
BASH
go run main.go
TEXT
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:

GO
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)
	}
}
BASH
go run main.go
TEXT
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:

GO
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")
}
BASH
go run main.go
# Pressione Ctrl+C para acionar saída graciosa
TEXT
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.

GO
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.

GO
// 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:

GO
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:

GO
// ❌ 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:

  1. select é a ferramenta central de concorrência do Go para multiplexação
  2. Quando múltiplos cases estão prontos simultaneamente, um é escolhido aleatoriamente para justiça
  3. for-select é o padrão padrão para escutar múltiplos channels
  4. Cuidado com vazamentos de memória de time.After em loops
  5. 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.

GO
// 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.

GO
// 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:

GO
// 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%