Multiplexación de canales con select en Go

select es la pieza fundamental que convierte a los canales en una herramienta de orquestación y no solo de paso de mensajes. A diferencia de un switch tradicional, que evalúa expresiones lógicas, el select monitoriza el estado de comunicación de múltiples canales simultáneamente. Su lógica interna le permite esperar a que uno de los canales esté listo para una operación de envío o recepción. Si varios casos están listos al mismo tiempo, el runtime de Go elige uno de forma pseudo-aleatoria para garantizar la equidad y evitar que un canal “gane” siempre por estar en una posición preferencial en el código.

Para usarlo correctamente, debes entender cuándo bloquea y cuándo no. Si usas un select sin la cláusula default, la goroutine se quedará bloqueada hasta que uno de los canales esté listo o el programa termine. Si incluyes un default, la operación se vuelve no bloqueante: si ningún canal está listo en ese instante preciso, se ejecuta el bloque default y la ejecución continúa. Usarás select siempre que necesites implementar timeouts, manejar cancelaciones mediante context.Context o realizar un polling no bloqueante de un estado. Si lo usas mal —por ejemplo, dejando un select vacío select{} o fallando al gestionar los canales en un bucle— puedes causar un deadlock (bloqueo mutuo) o un consumo de CPU del 100% por un bucle infinito de chequeo innecesario.

package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	// Creamos un contexto con un timeout de 1 segundo.
	// Es la forma estándar de evitar que una operación se cuelgue para siempre.
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel() // Siempre liberar el contexto para evitar fugas de memoria.

	// Canal para recibir resultados de un proceso pesado.
	resultCh := make(chan string)

	// Simulamos una tarea asíncrona que tarda 2 segundos.
	// Como el timeout es de 1 segundo, el context ganará la carrera.
	go func() {
		time.Sleep(2 * time.Second)
		resultCh <- "Tarea completada con éxito"
	}()

	// 1. Ejemplo de Multiplexación con Timeout/Cancelación
	fmt.Println("Iniciando operación...")
	select {
	case res := <-resultCh:
		fmt.Printf("Resultado recibido: %s\n", res)
	case <-ctx.Done():
		// ctx.Done() se cierra cuando el timeout se cumple o se llama a cancel().
		fmt.Printf("Operación abortada: %v\n", ctx.Err())
	}

	// 2. Ejemplo de Polling no bloqueante con default
	// Queremos revisar si hay algo en un canal sin quedarnos parados.
	checkCh := make(chan int)
	fmt.Println("\nIniciando chequeo no bloqueante...")

	for i := 0; i < 3; i++ {
		select {
		case val := <-checkCh:
			fmt.Printf("Leído valor: %d\n", val)
		default:
			// Si checkCh no tiene datos listos, entramos aquí inmediatamente.
			fmt.Println("Nada listo aún, haciendo otra cosa...")
		}
		time.Sleep(200 * time.Millisecond)
	}
}

Desglose del funcionamiento

En el primer bloque, el select actúa como un árbitro. El case res := <-resultCh está esperando un valor, pero como la goroutine tarda 2 segundos y el context.WithTimeout expira en 1 segundo, el runtime de Go detecta que el canal ctx.Done() se ha cerrado antes que resultCh esté listo. Esto permite que tu programa siga adelante en lugar de quedarse bloqueado esperando una respuesta que nunca llegará.

En el segundo bloque, observa el uso de default. En cada iteración del bucle for, el select pregunta: “¿Está checkCh listo para entregar algo?”. Como no hemos enviado nada a checkCh en la goroutine principal, el canal no tiene datos pendientes. En un select normal, el programa se detendría ahí, pero gracias al default, el flujo salta inmediatamente al bloque de “nada listo aún”, permitiendo que el bucle continúe su ejecución.

El uso de ctx.Done() dentro de un select es el patrón de diseño más importante para construir sistemas distribuidos robustos en Go. Te permite propagar señales de cancelación a través de múltiples capas de llamadas, asegurando que si un cliente cierra una conexión, todas las goroutines trabajando en esa petición se detengan limpiamente.

El error frecuente

Un error clásico que destruye el rendimiento de una aplicación es usar select con un default dentro de un bucle infinito sin ninguna otra forma de control o pausa.

// ERROR: Esto causará un uso de CPU del 100%
for {
    select {
    case msg := <-ch:
        fmt.Println(msg)
    default:
        // Al no haber nada en 'ch', el 'default' se ejecuta 
        // millones de veces por segundo, consumiendo todo el CPU.
    }
}

Si necesitas hacer polling (revisar un canal periódicamente), nunca uses un default vacío. En su lugar, usa un time.Ticker o añade un time.Sleep dentro del default para ceder el control al planificador del runtime.

136

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio