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 select com default para operações não bloqueantes, mas evite polling excessivo que pode consumir CPU.
  • Timeouts com time.After sã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 com time.NewTimer.
  • Em multiplexação, se vários canais estiverem prontos, a escolha é aleatória; não assuma ordem.
  • Evite select vazio (sem casos) em goroutines, pois isso bloqueia para sempre (equivalente a um for {}).
  • Use select para implementar padrões como fan-in, fan-out, orquestração de goroutines e cancelamento com canais.

Referências

Exercícios

  1. Crie um programa que use select para 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)
    		}
    	}
    }
  2. 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 select com default.

    ✓ 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")
    	}
    }
  3. 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.After e select.

    ✓ 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)
    	}
    }
  4. Crie um programa que use select para 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)
    		}
    	}
    }
  5. Escreva um programa que simule um servidor que aceita requisições de dois tipos (por exemplo, "ler" e "escrever") em canais separados. Use select para 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)
    		}
    	}
    }