O que é Select em Go?

O select é uma instrução de controle de fluxo em Go que permite esperar simultaneamente em múltiplas operações de channel. Enquanto o switch compara valores, o select compara operações de comunicação — qual channel está pronto para enviar ou receber. É a peça fundamental para orquestrar goroutines concorrentes de forma elegante e sem bloqueio.

Pense no select como um garçom em um restaurante observando múltiplas mesas ao mesmo tempo: assim que qualquer mesa levanta a mão (um channel fica pronto), ele atende. Se várias mesas levantam simultaneamente, ele escolhe uma aleatoriamente. Se nenhuma mesa precisa de atenção e existe uma tarefa padrão (default), ele faz essa tarefa em vez de ficar parado esperando.

O select é o que torna os padrões de concorrência em Go tão poderosos e expressivos. Sem ele, coordenar múltiplos channels seria extremamente verboso e propenso a erros. Combinado com goroutines e channels, o select forma a tríade da concorrência Go — os três pilares que fazem da linguagem uma das melhores para sistemas concorrentes e distribuídos.

Sintaxe básica

A estrutura do select é similar ao switch, mas cada case é uma operação de channel:

select {
case msg := <-canal1:
    fmt.Println("Recebido de canal1:", msg)
case msg := <-canal2:
    fmt.Println("Recebido de canal2:", msg)
case canal3 <- "hello":
    fmt.Println("Enviado para canal3")
}

Regras do select

  1. Cada case deve ser uma operação de channel — envio (ch <- valor) ou recebimento (<-ch)
  2. Se múltiplos cases estão prontos — Go escolhe um aleatoriamente (não sequencialmente!)
  3. Se nenhum case está pronto — o select bloqueia até algum ficar pronto
  4. Se existe default — executa o default em vez de bloquear

A aleatoriedade na escolha entre cases prontos é intencional — evita starvation e garante que todos os channels tenham chance justa de serem processados.

Multiplexando channels

O caso de uso mais comum é processar mensagens de múltiplas fontes concorrentes:

func processar(ctx context.Context) {
    pedidos := make(chan Pedido)
    pagamentos := make(chan Pagamento)
    erros := make(chan error)

    // Goroutines produtoras
    go receberPedidos(ctx, pedidos)
    go processarPagamentos(ctx, pagamentos)

    for {
        select {
        case pedido := <-pedidos:
            fmt.Printf("Novo pedido: %s\n", pedido.ID)
            registrarPedido(pedido)

        case pagamento := <-pagamentos:
            fmt.Printf("Pagamento recebido: R$%.2f\n", pagamento.Valor)
            confirmarPagamento(pagamento)

        case err := <-erros:
            fmt.Printf("Erro: %v\n", err)
            notificarAdmin(err)

        case <-ctx.Done():
            fmt.Println("Encerrando processamento")
            return
        }
    }
}

Cada iteração do loop espera o próximo evento de qualquer fonte. Esse padrão é a base de event loops em microsserviços Go e aplicações de streaming com Kafka.

Default case — operações não-bloqueantes

O default transforma o select em uma operação não-bloqueante:

// Tentar enviar sem bloquear
select {
case ch <- mensagem:
    fmt.Println("Mensagem enviada")
default:
    fmt.Println("Channel cheio, descartando mensagem")
}

// Tentar receber sem bloquear
select {
case msg := <-ch:
    fmt.Println("Recebido:", msg)
default:
    fmt.Println("Nenhuma mensagem disponível")
}

Esse padrão é essencial para implementar buffers com backpressure, polling e filas com descarte controlado. Em sistemas de alta performance, o default evita que goroutines fiquem bloqueadas indefinidamente esperando channels lentos.

Channel com capacidade limitada e descarte

// Buffer de eventos com descarte do mais antigo
type EventBuffer struct {
    ch chan Evento
}

func NovoEventBuffer(tamanho int) *EventBuffer {
    return &EventBuffer{ch: make(chan Evento, tamanho)}
}

func (b *EventBuffer) Publicar(evento Evento) {
    select {
    case b.ch <- evento:
        // Evento adicionado com sucesso
    default:
        // Buffer cheio: descartar o mais antigo e tentar novamente
        <-b.ch
        b.ch <- evento
    }
}

Timeout com select

Implementar timeouts é um dos padrões mais importantes com select:

func buscarComTimeout(url string, timeout time.Duration) ([]byte, error) {
    resultado := make(chan []byte, 1)
    erros := make(chan error, 1)

    go func() {
        dados, err := fetchURL(url)
        if err != nil {
            erros <- err
            return
        }
        resultado <- dados
    }()

    select {
    case dados := <-resultado:
        return dados, nil
    case err := <-erros:
        return nil, err
    case <-time.After(timeout):
        return nil, fmt.Errorf("timeout após %v buscando %s", timeout, url)
    }
}

time.After(d) retorna um channel que recebe um valor após a duração especificada. Combinado com select, cria um padrão de timeout limpo e idiomático.

Em produção, prefira usar context com WithTimeout em vez de time.After, pois o context propaga cancelamento por toda a cadeia de chamadas e é mais integrado com a biblioteca padrão.

Timeout com context (preferido)

func buscarComContext(ctx context.Context, url string) ([]byte, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    resultado := make(chan []byte, 1)
    erros := make(chan error, 1)

    go func() {
        dados, err := fetchURL(url)
        if err != nil {
            erros <- err
            return
        }
        resultado <- dados
    }()

    select {
    case dados := <-resultado:
        return dados, nil
    case err := <-erros:
        return nil, err
    case <-ctx.Done():
        return nil, ctx.Err()
    }
}

Done channel — sinalização de parada

O padrão done channel usa um channel fechado para sinalizar que goroutines devem parar:

func worker(done <-chan struct{}, tarefas <-chan Tarefa) {
    for {
        select {
        case <-done:
            fmt.Println("Worker encerrando")
            return
        case tarefa := <-tarefas:
            fmt.Printf("Processando: %s\n", tarefa.Nome)
            executar(tarefa)
        }
    }
}

func main() {
    done := make(chan struct{})
    tarefas := make(chan Tarefa, 100)

    // Iniciar workers
    for i := 0; i < 5; i++ {
        go worker(done, tarefas)
    }

    // Enviar tarefas...
    for _, t := range listaTarefas {
        tarefas <- t
    }

    // Sinalizar parada para TODOS os workers
    close(done) // fechar o channel sinaliza todos os select
}

Fechar um channel faz com que todas as operações de recebimento retornem imediatamente com o valor zero. Isso torna close(done) uma forma eficiente de broadcasting — um único close notifica todas as goroutines que estão monitorando esse channel via select.

Fan-in — unificando múltiplos channels

O padrão fan-in combina múltiplas fontes em um único channel:

func fanIn(channels ...<-chan string) <-chan string {
    var wg sync.WaitGroup
    merged := make(chan string)

    // Iniciar uma goroutine para cada channel de entrada
    for _, ch := range channels {
        wg.Add(1)
        go func(c <-chan string) {
            defer wg.Done()
            for msg := range c {
                merged <- msg
            }
        }(ch)
    }

    // Fechar o channel de saída quando todos terminarem
    go func() {
        wg.Wait()
        close(merged)
    }()

    return merged
}

// Uso
fonte1 := gerarDados("API-A")
fonte2 := gerarDados("API-B")
fonte3 := gerarDados("API-C")

for msg := range fanIn(fonte1, fonte2, fonte3) {
    fmt.Println(msg)
}

Fan-in é fundamental em pipelines de dados, agregação de logs e quando múltiplas goroutines produzem resultados que precisam ser consumidos por um único processador.

Select com envio e recebimento

O select pode misturar operações de envio e recebimento no mesmo bloco:

func broker(entrada <-chan Mensagem, saida chan<- Mensagem, done <-chan struct{}) {
    var fila []Mensagem

    for {
        // Decidir dinamicamente quais cases incluir
        var enviarCh chan<- Mensagem
        var proxima Mensagem

        if len(fila) > 0 {
            enviarCh = saida
            proxima = fila[0]
        }

        select {
        case msg := <-entrada:
            fila = append(fila, msg)

        case enviarCh <- proxima:
            fila = fila[1:]

        case <-done:
            return
        }
    }
}

Esse padrão de “channel condicional” é poderoso: quando enviarCh é nil, o case de envio nunca é selecionado, efetivamente desabilitando-o. Channels nil bloqueiam para sempre em select, o que os torna úteis como “interruptores” para habilitar ou desabilitar cases dinamicamente.

Select vazio — bloqueio permanente

Um select{} sem cases bloqueia a goroutine para sempre:

func main() {
    go servidor()
    select{} // bloqueia main para sempre
}

Esse padrão é usado em programas que precisam rodar indefinidamente, como servidores e daemons. É equivalente a for {} mas mais idiomático e consume menos CPU, pois a goroutine é colocada em estado de espera pelo scheduler em vez de girar em busy loop.

Ticker com select — execução periódica

func monitorar(ctx context.Context) {
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            status := verificarSaude()
            reportar(status)
        case <-ctx.Done():
            fmt.Println("Monitoramento encerrado")
            return
        }
    }
}

Sempre chame ticker.Stop() com defer para liberar recursos. Tickers sem stop continuam enviando valores e causam vazamento de goroutines.

Boas práticas com select

  1. Sempre inclua um case de cancelamento — via done channel ou context
  2. Use channels bufferizados quando o produtor não pode bloquear — previne goroutine leaks
  3. Prefira context sobre time.After — context propaga cancelamento por toda a cadeia
  4. Channels nil desabilitam cases — use como interruptores dinâmicos
  5. Evite select com default em loops apertados — consome CPU desnecessariamente
  6. Documente qual case tem prioridade — a escolha é aleatória por design

Para aprofundar em padrões de concorrência, explore o glossário de channels, mutex e os padrões de concorrência em Go.

Perguntas frequentes (FAQ)

O select em Go tem prioridade entre cases?

Não. Quando múltiplos cases estão prontos simultaneamente, Go escolhe um aleatoriamente usando um gerador pseudo-aleatório. Essa decisão de design evita starvation — nenhum channel é favorecido sobre outro. Se você precisa de prioridade, use selects aninhados: primeiro tente o channel prioritário, depois o restante.

Qual a diferença entre select e switch em Go?

O switch compara valores ou expressões booleanas, enquanto o select espera operações de channel. O select bloqueia até que alguma operação de comunicação esteja pronta (a menos que tenha default). Ambos usam sintaxe de case, mas são mecanismos completamente diferentes — select é exclusivo para concorrência.

Como evitar goroutine leaks com select?

Sempre inclua um case de cancelamento (via done channel ou ctx.Done()) em loops com select. Goroutines que ficam bloqueadas em select sem forma de sair são vazamentos de memória — elas nunca são coletadas pelo garbage collector. Use o detector de goroutine leaks em testes para verificar.

Posso usar select com um único channel?

Sim, mas geralmente não faz sentido sem default — um select com um único case sem default é equivalente a um recebimento direto do channel. O valor do select está na multiplexação: esperar múltiplos channels, adicionar timeouts ou combinar com default para operações não-bloqueantes.