Select: multiplexando channels

1. Introdução ao select e multiplexação de canais

Em Go, o select é uma estrutura de controle que permite que uma goroutine aguarde múltiplas operações de comunicação em canais simultaneamente. Funciona de forma análoga ao switch, mas especificamente para canais: cada case representa uma operação de envio ou recebimento em um canal. O select bloqueia até que um dos casos possa ser executado.

A multiplexação de canais é a capacidade de escutar vários canais ao mesmo tempo e reagir ao primeiro que estiver pronto. Isso é fundamental para construir sistemas concorrentes responsivos e eficientes.

Exemplo básico:

package main

import (
    "fmt"
    "time"
)

func main() {
    canal1 := make(chan string)
    canal2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        canal1 <- "mensagem do canal 1"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        canal2 <- "mensagem do canal 2"
    }()

    select {
    case msg1 := <-canal1:
        fmt.Println("Recebido:", msg1)
    case msg2 := <-canal2:
        fmt.Println("Recebido:", msg2)
    }
}

Neste exemplo, o select aguarda até que uma das goroutines envie dados para seu respectivo canal. Como o canal1 recebe dados primeiro (após 1 segundo), a saída será "Recebido: mensagem do canal 1".

2. Sintaxe e funcionamento do select

A estrutura básica do select é:

select {
case <-canalA:
    // executado quando canalA tem dados
case canalB <- valor:
    // executado quando pudermos enviar para canalB
default:
    // executado se nenhum caso estiver pronto
}

Uma característica importante é a seleção aleatória quando múltiplos casos estão prontos simultaneamente. Go implementa fairness (justiça) escolhendo aleatoriamente entre os casos prontos, evitando starvation.

package main

import (
    "fmt"
    "time"
)

func main() {
    canal := make(chan string, 1)
    canal <- "dado"

    select {
    case msg := <-canal:
        fmt.Println("Caso 1:", msg)
    case msg := <-canal:
        fmt.Println("Caso 2:", msg)
    default:
        fmt.Println("Nenhum caso pronto")
    }
}

Apesar de termos apenas um dado no canal, o select só executará um caso (aleatoriamente, se houvesse múltiplos canais prontos).

3. Tratamento de canais nulos e o caso default

Canais nil em um select são ignorados — nunca são selecionados. Isso pode ser usado para desativar dinamicamente um caso.

O default permite operações não bloqueantes:

package main

import (
    "fmt"
    "time"
)

func main() {
    canal := make(chan int, 1)

    // Tentativa de leitura não bloqueante
    select {
    case val := <-canal:
        fmt.Println("Valor recebido:", val)
    default:
        fmt.Println("Canal vazio, continuando...")
    }

    // Envio não bloqueante
    select {
    case canal <- 42:
        fmt.Println("Valor enviado com sucesso")
    default:
        fmt.Println("Canal cheio, descartando valor")
    }
}

4. Timeouts com select e time.After

Timeouts são implementados elegantemente com time.After:

package main

import (
    "fmt"
    "time"
)

func leituraComTimeout(canal <-chan string, timeout time.Duration) (string, error) {
    select {
    case msg := <-canal:
        return msg, nil
    case <-time.After(timeout):
        return "", fmt.Errorf("timeout após %v", timeout)
    }
}

func main() {
    canal := make(chan string)

    go func() {
        time.Sleep(3 * time.Second)
        canal <- "dado atrasado"
    }()

    resultado, err := leituraComTimeout(canal, 2*time.Second)
    if err != nil {
        fmt.Println("Erro:", err)
    } else {
        fmt.Println("Resultado:", resultado)
    }
}

Cuidado: Em loops, time.After cria um novo timer a cada iteração, que pode vazar memória se o loop for muito longo. Prefira time.NewTimer e reutilize-o.

5. select vazio e loops infinitos

Um select {} bloqueia para sempre, pois não há casos para selecionar. É útil para manter uma goroutine principal viva:

package main

import (
    "fmt"
    "time"
)

func trabalhador(id int, tarefas <-chan string) {
    for {
        select {
        case tarefa, ok := <-tarefas:
            if !ok {
                fmt.Printf("Trabalhador %d encerrando\n", id)
                return
            }
            fmt.Printf("Trabalhador %d processando: %s\n", id, tarefa)
        }
    }
}

func main() {
    tarefas := make(chan string, 3)
    tarefas <- "tarefa 1"
    tarefas <- "tarefa 2"
    close(tarefas)

    go trabalhador(1, tarefas)
    time.Sleep(100 * time.Millisecond)
    // select {} // manteria o programa rodando indefinidamente
}

6. Cancelamento com select e done channel

O padrão done channel é essencial para encerramento limpo de goroutines:

package main

import (
    "fmt"
    "time"
)

func worker(done <-chan struct{}, tarefas <-chan int) {
    for {
        select {
        case tarefa := <-tarefas:
            fmt.Printf("Processando tarefa %d\n", tarefa)
            time.Sleep(100 * time.Millisecond)
        case <-done:
            fmt.Println("Worker recebeu sinal de parada")
            return
        }
    }
}

func main() {
    tarefas := make(chan int, 5)
    done := make(chan struct{})

    go worker(done, tarefas)

    for i := 1; i <= 3; i++ {
        tarefas <- i
    }

    close(done) // sinaliza parada
    time.Sleep(200 * time.Millisecond)
    fmt.Println("Programa finalizado")
}

7. Padrão de tentativa (try-send / try-receive)

Envios e recebimentos não bloqueantes são úteis para buffers cheios ou descarte de mensagens antigas:

package main

import (
    "fmt"
    "time"
)

func main() {
    buffer := make(chan string, 2)

    // Try-send: envia apenas se houver espaço
    for i := 0; i < 5; i++ {
        select {
        case buffer <- fmt.Sprintf("msg %d", i):
            fmt.Printf("Mensagem %d enviada\n", i)
        default:
            fmt.Printf("Buffer cheio, mensagem %d descartada\n", i)
        }
    }

    // Try-receive: recebe apenas se houver dados
    for i := 0; i < 5; i++ {
        select {
        case msg := <-buffer:
            fmt.Println("Lido:", msg)
        default:
            fmt.Println("Buffer vazio")
        }
    }
}

8. Boas práticas e armadilhas comuns

Evite select sem controle de saída em loops:

// RUIM: loop infinito sem condição de saída
for {
    select {
    case msg := <-canal:
        processar(msg)
    }
}

// BOM: com done channel
for {
    select {
    case msg := <-canal:
        processar(msg)
    case <-done:
        return
    }
}

Cuidado com time.After em loops longos:

// RUIM: vazamento de memória
for {
    select {
    case msg := <-canal:
        processar(msg)
    case <-time.After(1 * time.Second):
        fmt.Println("timeout")
    }
}

// BOM: reutilizando timer
timer := time.NewTimer(1 * time.Second)
defer timer.Stop()
for {
    timer.Reset(1 * time.Second)
    select {
    case msg := <-canal:
        processar(msg)
    case <-timer.C:
        fmt.Println("timeout")
    }
}

Prefira select com done channel para encerramento limpo: Sempre forneça uma maneira de encerrar goroutines graciosamente, evitando deadlocks e vazamentos.

Referências