`select`: multiplexar canales, `default` y timeouts
select es el switch para canales. Permite esperar a que cualquiera de varias operaciones de canal esté lista — el corazón de la concurrencia idiomática en Go.
¿Qué hace select?
select bloquea hasta que uno de sus case esté listo (un envío o recepción que no bloquearía). Si varios están listos a la vez, elige uno al azar. Si ninguno está listo y hay default, ejecuta el default sin bloquear.
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(50 * time.Millisecond)
ch1 <- "desde ch1"
}()
go func() {
time.Sleep(100 * time.Millisecond)
ch2 <- "desde ch2"
}()
// Esperamos al primero que llegue
select {
case v := <-ch1:
fmt.Println("ganó ch1:", v)
case v := <-ch2:
fmt.Println("ganó ch2:", v)
}
}ganó ch1: desde ch1Mezclar envíos y recepciones
Cada case puede ser un envío (ch <- v) o una recepción (v := <-ch). select los considera todos a la vez.
package main
import "fmt"
func main() {
entrada := make(chan int, 1)
salida := make(chan int, 1)
entrada <- 7 // dejamos un valor listo para recibir
select {
case v := <-entrada:
fmt.Println("recibido de entrada:", v)
case salida <- 99:
fmt.Println("enviado a salida")
}
}recibido de entrada: 7Cuando varios case están listos al mismo tiempo, el runtime elige uno pseudoaleatoriamente. Esto evita inanición (que un canal nunca se atienda) y obliga a no asumir orden de prioridad. Si necesitas prioridad, anida un select con default.
default: select no bloqueante
Con default, select no bloquea: si ningún canal está listo, ejecuta el bloque por defecto. Sirve para hacer envíos/recepciones oportunistas.
package main
import "fmt"
func main() {
ch := make(chan int, 1)
// Envío no bloqueante: si el canal está lleno, descarta
select {
case ch <- 1:
fmt.Println("enviado 1")
default:
fmt.Println("canal lleno, descartamos")
}
// Segundo intento: el buffer ya está lleno
select {
case ch <- 2:
fmt.Println("enviado 2")
default:
fmt.Println("canal lleno, descartamos")
}
// Recepción no bloqueante
select {
case v := <-ch:
fmt.Println("recibido:", v)
default:
fmt.Println("nada que recibir")
}
}enviado 1
canal lleno, descartamos
recibido: 1Timeouts con time.After
time.After(d) devuelve un <-chan Time que recibe un valor pasados d nanosegundos. Combinado con select da timeouts elegantes.
package main
import (
"fmt"
"time"
)
func consulta() <-chan string {
out := make(chan string)
go func() {
time.Sleep(2 * time.Second) // simula trabajo lento
out <- "resultado"
}()
return out
}
func main() {
select {
case r := <-consulta():
fmt.Println("respuesta:", r)
case <-time.After(500 * time.Millisecond):
fmt.Println("timeout: la consulta tardó demasiado")
}
}timeout: la consulta tardó demasiadoCada llamada a time.After crea un timer que vive hasta vencer aunque ya no te interese. Dentro de bucles muy frecuentes usa time.NewTimer y Stop() para reciclarlo, o mejor aún context.WithTimeout (capítulo siguiente).
Bucle con canal de cancelación
Un patrón muy frecuente: una goroutine trabaja en bucle hasta que su canal de cancelación recibe algo.
package main
import (
"fmt"
"time"
)
func trabajador(done <-chan struct{}) {
tick := time.Tick(100 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("trabajando...")
case <-done:
fmt.Println("recibí cancelación, salgo")
return
}
}
}
func main() {
done := make(chan struct{})
go trabajador(done)
time.Sleep(350 * time.Millisecond)
close(done) // cerrar es la forma idiomática de "broadcast"
time.Sleep(50 * time.Millisecond)
}trabajando...
trabajando...
trabajando...
recibí cancelación, salgoCerrar un canal done notifica a todas las goroutines que escuchan <-done al mismo tiempo: una recepción de canal cerrado nunca bloquea. Es la base de la cancelación con context.Context.