Esperar en varios channels
select se parece a un switch, pero cada caso es un envío o una recepción en un channel. Se bloquea hasta que un caso puede continuar y entonces ejecuta ese.
Con estos retardos, el primer select recibe el resultado rápido y el segundo el lento. Ninguna recepción tiene que esperar al otro channel. Un <-slow a secas seguido de <-fast los atendería en un orden fijo, llegara primero el que llegara.
Cómo se evalúa select:
- Todas las expresiones de channel y los valores que se envían se evalúan una vez, en orden de código, cuando empieza el
select. - Si uno o varios casos están listos, se elige uno de ellos al azar.
- Si ninguno está listo y hay un
default, se ejecuta eldefault. - Si no, la goroutine se bloquea hasta que algún caso queda listo.
Un select {} vacío se bloquea para siempre. A veces lo verás al final de main en programas cuyo trabajo real ocurre en otras goroutines.
Elección aleatoria entre los casos listos
Cuando varios casos están listos a la vez, select no prefiere el primero de la lista. Este programa llena dos channels con buffer y luego hace select 1000 veces:
El reparto entre countA y countB cambia en cada ejecución y ronda los 500 cada uno. La elección aleatoria es deliberada: evita que un channel con mucho tráfico deje sin turno a los demás. Si necesitas prioridad, mira el patrón más abajo.
Operaciones no bloqueantes con default
Con un caso default, select nunca se bloquea. Eso convierte un envío o una recepción en una operación de "intento":
Descartar trabajo cuando un buffer está lleno es la forma de aliviar carga o emitir métricas sin frenar nunca a quien llama.
No pongas un default en un select dentro de un bucle for solo para "consultar" los channels una y otra vez. Sin nada listo, el bucle gira al 100% de CPU. Bloquéate, y añade un caso de timeout si necesitas despertar de vez en cuando.
Timeouts
time.After(d) devuelve un channel que recibe una vez pasado d. Ponlo a competir con el trabajo real:
La primera llamada devuelve "data". La segunda devuelve el error de timeout a los 50 ms, mucho antes de que el worker hubiera terminado. El channel de resultado tiene un buffer de 1 a propósito. Cuando gana el timeout, nadie recibe nunca de result; con un channel sin buffer, la goroutine del worker se bloquearía en su envío para siempre y se fugaría.
En un bucle, time.After crea un temporizador nuevo en cada iteración, que es justo lo correcto para un timeout de inactividad por mensaje ("ningún mensaje en 1 segundo"). Para un deadline global a lo largo de muchas operaciones, crea un único temporizador o context antes del bucle. Desde Go 1.23, los temporizadores a los que ya nada hace referencia se recolectan aunque no hayan saltado, así que time.After en un bucle ya no retiene memoria hasta que salta cada temporizador, como ocurría en versiones anteriores (hace falta go 1.23 o posterior en go.mod).
Bucles for-select y channels de salida
Una goroutine que se ejecuta hasta que se le dice que pare es un bucle for alrededor de un select con un caso para el trabajo y otro para parar:
Cerrar quit en lugar de enviar por él es lo idiomático: un cierre lo ven todos los receptores, ahora y después, así que un solo close detiene a cualquier número de workers. El channel done permite a main esperar hasta que el worker ha retornado de verdad.
En código real, el channel de salida suele ser un context.Context: case <-ctx.Done():. Funciona igual (Done() devuelve un channel que se cierra al cancelar) y además lleva deadlines y el motivo de la parada. La página de context lo explica.
break dentro de select
break en un caso de select sale del select, no del for que lo contiene. Es una fuente habitual de bucles que nunca terminan. Usa return, o ponle una etiqueta al bucle:
Prioridad entre channels
Como select elige al azar, no puedes ordenar los casos dentro de una misma sentencia. Para que un channel gane siempre que tenga algo, compruébalo antes por separado:
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
select {
case <-ctx.Done():
return ctx.Err()
case job := <-jobs:
handle(job)
}
}
El primer select retorna de inmediato si la cancelación ya ocurrió. Sin él, un flujo constante de trabajos podría seguir ganando la elección aleatoria durante un rato después de cancelar ctx.
Los channels nil desactivan un caso
Un envío o una recepción en un channel nil nunca está listo, así que un caso sobre un channel nil queda en la práctica desactivado. Asignar nil a una entrada cerrada es la forma de dejar de hacer select sobre ella mientras sigues con las demás; la página de channels muestra un bucle de combinación hecho así. El mismo truco sirve para activar y desactivar un timeout: mantén var timeout <-chan time.Time a nil hasta que lo necesites, y entonces asígnale time.After(d).
Errores comunes
- Esperar el orden del código. El primer caso de la lista no tiene preferencia.
- Un bucle activo con
default. Unfor { select { ... default: } }sin nada que hacer quema un núcleo de CPU. - Fugar al perdedor. Cuando gana un timeout, la goroutine que habría enviado el resultado tiene que poder terminar. Dale a su channel un buffer de 1.
- Un
breakque solo sale delselect. Usa una etiqueta oreturn. - Un
time.Afterpor iteración usado como deadline global. Se reinicia en cada iteración; crea el deadline una vez, fuera del bucle.
Preguntas frecuentes
¿Qué hace select en Go?
select espera hasta que una de sus operaciones de channel (un envío o una recepción) puede continuar, y entonces ejecuta ese caso. Si hay varios listos a la vez, elige uno al azar. Sin un caso default se bloquea hasta que algún caso está listo; con default nunca se bloquea.
¿Cómo pongo un timeout a la recepción de un channel en Go?
Pon la recepción y un temporizador en un mismo select: select { case v := <-ch: use(v); case <-time.After(2 * time.Second): return errTimeout }. Gana lo que ocurra primero. Dentro de un bucle, o cuando quien llama ya tiene un deadline, usa un context.Context con context.WithTimeout y haz select sobre ctx.Done().
¿select en Go elige los casos en orden?
No. Cuando hay más de un caso listo, Go elige de forma uniformemente aleatoria, así que ningún caso puede dejar sin turno a los demás. Si necesitas prioridad, comprueba antes el channel prioritario en su propio select con un default, y después pasa a un select sobre todos los channels.
¿Por qué break no sale de mi bucle for-select?
Dentro de un select, break sale solo de la sentencia select, no del for que la rodea. Usa return, o pon una etiqueta al bucle (loop: for { select { case <-done: break loop } }).