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

Exercícios

  1. Escreva um programa que use select para 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)
    	}
    }
    
  2. Modifique o programa anterior para incluir uma cláusula default que 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")
    	}
    }
    
  3. Crie um programa que use select com time.After para 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!")
    	}
    }
    
  4. Escreva um programa que utilize um loop com select para 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)
    		}
    	}
    }
    
  5. 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 select para 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")
    	}
    }