select en Go: multiplexar canales, timeout con time.After y caso default

La sentencia select de Go permite a una goroutine esperar en varios canales simultáneamente y procesar el primero que esté listo. Es el equivalente a un switch pero para operaciones de canal, y resulta fundamental para implementar timeouts, cancelaciones y lógica de multiplexación.

Sintaxis básica

select evalúa todos sus casos y elige uno al azar si varios están listos al mismo tiempo. Si ninguno está listo, bloquea hasta que alguno lo esté:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "uno"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "dos"
    }()

    // Espera a que llegue algo en cualquiera de los dos canales
    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch1:
            fmt.Println("recibido de ch1:", msg)
        case msg := <-ch2:
            fmt.Println("recibido de ch2:", msg)
        }
    }
}

Timeout con time.After

time.After(d) devuelve un canal que recibe un valor pasado el tiempo indicado. Combinado con select, implementa timeouts sin necesidad de goroutines adicionales:

func buscarConTimeout(query string) (string, error) {
    resultados := make(chan string, 1)

    go func() {
        // Simulación de una búsqueda lenta
        time.Sleep(300 * time.Millisecond)
        resultados <- "resultado para: " + query
    }()

    select {
    case res := <-resultados:
        return res, nil
    case <-time.After(200 * time.Millisecond):
        return "", fmt.Errorf("timeout: la búsqueda tardó demasiado")
    }
}

func main() {
    res, err := buscarConTimeout("Go select")
    if err != nil {
        fmt.Println("Error:", err) // Error: timeout: la búsqueda tardó demasiado
        return
    }
    fmt.Println(res)
}

Caso default: operaciones no bloqueantes

El caso default se ejecuta si ningún otro caso está listo. Convierte una operación de canal bloqueante en no bloqueante:

func intentarEnviar(ch chan<- int, valor int) bool {
    select {
    case ch <- valor:
        return true
    default:
        return false // canal lleno o sin receptor, sin bloquear
    }
}

func leerSiHay(ch <-chan int) (int, bool) {
    select {
    case v := <-ch:
        return v, true
    default:
        return 0, false // canal vacío, sin bloquear
    }
}

Cancelación con canal done

El patrón done usa un canal cerrado para señalizar la cancelación a múltiples goroutines. Cuando se cierra el canal, todos los receptores reciben el zero value inmediatamente:

func trabajador(id int, jobs <-chan int, done <-chan struct{}) {
    for {
        select {
        case j, ok := <-jobs:
            if !ok {
                fmt.Printf("worker %d: canal cerradon", id)
                return
            }
            fmt.Printf("worker %d procesando trabajo %dn", id, j)
        case <-done:
            fmt.Printf("worker %d canceladon", id)
            return
        }
    }
}

func main() {
    jobs := make(chan int, 10)
    done := make(chan struct{})

    for i := 1; i <= 3; i++ {
        go trabajador(i, jobs, done)
    }

    for i := 0; i < 5; i++ {
        jobs <- i
    }
    time.Sleep(100 * time.Millisecond)
    close(done) // cancela todos los workers
}

Bucle de procesado de mensajes

Un patrón frecuente en servidores es combinar select con un for para procesar mensajes de distintas fuentes hasta que llegue la señal de parada:

func servidor(peticiones <-chan string, stop <-chan struct{}) {
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case req := <-peticiones:
            fmt.Println("procesando:", req)
        case t := <-ticker.C:
            fmt.Println("healthcheck a las", t.Format("15:04:05"))
        case <-stop:
            fmt.Println("servidor detenido")
            return
        }
    }
}

Antipatrón: busy wait con default

Añadir un default vacío dentro de un bucle consume CPU al 100% porque el goroutine nunca se bloquea:

// MAL: busy wait — consume CPU sin parar
for {
    select {
    case v := <-ch:
        procesar(v)
    default:
        // sin bloqueo ? bucle continuo consumiendo CPU
    }
}

// BIEN: bloquear en el select sin default
for v := range ch {
    procesar(v)
}

Reserva el default para cuando realmente necesites un intento no bloqueante puntual, no para bucles continuos.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP