select en Go: esperar en múltiples canales y el caso por defecto

select permite a una goroutine esperar en múltiples operaciones de canal simultáneamente. Es el equivalente del switch para canales: se bloquea hasta que uno de sus case puede proceder y ejecuta ese bloque. Si varios cases están listos al mismo tiempo, elige uno al azar. Es fundamental para implementar timeouts, cancelaciones y patrones fan-in.

select básico

package main

import (
    "fmt"
    "time"
)

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

    go func() {
        time.Sleep(200 * time.Millisecond)
        ch1 <- "canal uno"
    }()

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch2 <- "canal dos"
    }()

    select {
    case msg := <-ch1:
        fmt.Println("Recibido de ch1:", msg)
    case msg := <-ch2:
        fmt.Println("Recibido de ch2:", msg)
    }
    // Imprime: Recibido de ch2: canal dos (llega antes)
}

case default: select no bloqueante

Añadir un case default hace que select no se bloquee: si ningún canal está listo, ejecuta el default inmediatamente:

func tryReceive(ch <-chan int) (int, bool) {
    select {
    case v := <-ch:
        return v, true
    default:
        return 0, false // no había nada disponible
    }
}

Timeout con time.After

time.After(d) devuelve un canal que recibe un valor después de la duración d. Combinado con select es la forma más sencilla de implementar un timeout:

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

    go func() {
        resultado := buscarEnBD(query) // operación lenta
        resultados <- resultado
    }()

    select {
    case r := <-resultados:
        return r, nil
    case <-time.After(2 * time.Second):
        return "", fmt.Errorf("timeout: la búsqueda tardó más de 2 segundos")
    }
}

Fan-in: combinar múltiples canales en uno

Fan-in recoge valores de varios canales y los envía por uno solo. select hace que esto sea elegante:

func fanIn(ch1, ch2 <-chan string) <-chan string {
    out := make(chan string)
    go func() {
        for {
            select {
            case v := <-ch1:
                out <- v
            case v := <-ch2:
                out <- v
            }
        }
    }()
    return out
}

Done channel: cancelar goroutines

El patrón done channel permite señalar a múltiples goroutines que deben detenerse. Cerrarlo difunde la señal a todos los receptores simultáneamente:

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

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

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

    for i := 0; i < 5; i++ {
        trabajo <- i
    }

    time.Sleep(100 * time.Millisecond)
    close(done) // señala a todos los workers que paren
    time.Sleep(50 * time.Millisecond)
}

select en un bucle: ticker periódico

func main() {
    ticker := time.NewTicker(500 * time.Millisecond)
    done := time.After(2 * time.Second)

    for {
        select {
        case t := <-ticker.C:
            fmt.Println("tick:", t.Format("15:04:05.000"))
        case <-done:
            ticker.Stop()
            fmt.Println("terminado")
            return
        }
    }
}

Prioridad entre cases

Cuando varios cases están listos, select elige uno al azar. Si necesitas prioridad, usa un select anidado o comprueba el canal prioritario con un default antes del select principal:

// prioritario primero, luego el resto
select {
case <-urgente:
    manejarUrgente()
default:
    select {
    case <-urgente:
        manejarUrgente()
    case v := <-normal:
        manejarNormal(v)
    }
}

COMPARTE ESTE ARTÍCULO

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