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.
