Saltar al contenido principal

Go desde cero, parte 12: channels y select

Curso: Go — Lección 14 de 17

> serie: aprende go de 0 a 100 | artículo 12 de 19

En el artículo anterior las goroutines volaban cada una a lo suyo y las esperábamos con time.Sleep, que es como sincronizar relojes gritando por la ventana. Hoy llega la herramienta de verdad: los channels en Go, las tuberías tipadas por las que las goroutines se pasan datos y, de regalo, se sincronizan.


Channels: enviar, recibir, bloquear

Un channel se crea con make y tiene tipo: por un chan string solo viajan strings (A Tour of Go, channels):

ch := make(chan string)

go func() {
  ch <- "hola desde la goroutine" // enviar
}()

msg := <-ch // recibir: BLOQUEA hasta que llegue algo
fmt.Println(msg)

// Sin buffer, envío y recepción se esperan mutuamente:
// es una cita, no un buzón.

La flecha <- lo es todo: hacia el channel envía, desde el channel recibe. Y el detalle que lo cambia todo: en un channel sin buffer ambas operaciones bloquean hasta que la otra parte aparece. Esa espera mutua ES la sincronización: cuando la recepción termina, sabes que el envío ocurrió. Nada de sleep a ojo.

Buffer, close y range

Con capacidad, el channel pasa de cita a buzón (A Tour of Go, buffered channels, range and close y especificación, close):

// Con buffer: el envío solo bloquea si está lleno.
buzon := make(chan int, 3)
buzon <- 1
buzon <- 2 // nadie recibe y no pasa nada, hay hueco

// close avisa de que no habrá más envíos.
close(buzon)

// range consume hasta que el channel se cierra y vacía:
for v := range buzon {
  fmt.Println(v)
}

// La coma ok también vive aquí:
v, ok := <-buzon // ok es false: cerrado y vacío

Reglas de la casa que evitan panics: quien cierra es siempre el EMISOR, nunca el receptor. Enviar a un channel cerrado es panic. Cerrar dos veces, también. Y recibir de uno cerrado devuelve zero values con ok en falso, que es justo lo que range usa para saber cuándo parar.

Las firmas pueden declarar dirección, y el compilador la hace cumplir:

// Direcciones en la firma: el compilador vigila.
func produce(salida chan<- int) { // solo enviar
  salida <- 42
}

func consume(entrada <-chan int) { // solo recibir
  fmt.Println(<-entrada)
}

select: escuchar varios channels a la vez

El select es un switch de operaciones de channel: espera hasta que alguno de sus cases pueda proceder y, si hay varios listos, elige uno al azar (A Tour of Go, select y especificación):

select {
  case msg := <-ch1:
    fmt.Println("llegó por ch1:", msg)
  case msg := <-ch2:
    fmt.Println("llegó por ch2:", msg)
  case <-time.After(2 * time.Second):
    fmt.Println("timeout: nadie contestó")
}

// Con default deja de bloquear:
select {
  case msg := <-ch1:
    fmt.Println(msg)
  default:
    fmt.Println("nada pendiente, sigo con mi vida")
}

Los dos patrones del ejemplo son fondo de armario: timeout con time.After para no esperar eternamente y default para consultar sin bloquear. Si trabajaste con Promise.race en JS, la idea te suena, pero esto funciona con cualquier número de operaciones en ambas direcciones.

Channels en Go: Diagrama pixelart ilustrando los channels en Go y select: una centralita con tres entradas, ch1, ch2 y time.After, compitiendo entre sí, y el primer case listo gana

[select es la centralita: atiende la primera línea que se libera.]

La fontanería completa

Con los channels en Go y select ya puedes montar el eslogan del artículo anterior: en vez de compartir el contador y protegerlo, una sola goroutine es dueña del dato y las demás le mandan mensajes (Effective Go, channels). El contador con carrera del reto pasado se arregla así: una goroutine suma lo que le llega por un channel y nadie más toca la variable.


Reto de la semana: pon a prueba los channels en Go

  • Ping pong: dos goroutines y un channel. Una envía «ping», la otra responde «pong», diez rondas y a casa.
  • Arregla el contador del artículo 11 con un channel: 1000 goroutines envían 1 y una acumuladora suma. Verifica con -race que la carrera desapareció.
  • Simula una consulta lenta con time.Sleep aleatorio y ponle un timeout de un segundo con select.
  • Bonus: productor que envía cinco números y cierra. Consumidor con range. Añade un segundo productor y descubre por qué cerrar se complica con varios emisores (pista para el artículo 14).

Channels en Go: nivel completado.

En el próximo artículo: patrones de concurrencia. Worker pools, fan-in, fan-out y pipelines, los planos de fontanería profesional.

> exit
Retrato pixel art de Jenniffer Cubillos

gracias por leer