Programación asíncrona en Go: de la teoría a la práctica
Go (Golang) es famoso por su soporte integrado de programación asíncrona. A diferencia de muchos otros lenguajes, donde la asincronía requiere bibliotecas complejas y callbacks, Go ofrece primitivas elegantes y potentes: gorutinas (goroutines) y canales (channels). En esta guía analizaremos todos los conceptos clave en la práctica, para que puedas escribir código eficiente y paralelo en Go.
¿Qué son las gorutinas (Goroutines)?
Una gorutina es un hilo de ejecución ligero, gestionado por el runtime de Go. A diferencia de los hilos del sistema, las gorutinas consumen muy pocos recursos (solo unos pocos kilobytes de pila), lo que permite ejecutar miles e incluso millones de gorutinas simultáneamente.
Iniciar una gorutina es muy sencillo: solo hay que añadir la palabra clave go antes de la llamada a una función:
package main
import ( "fmt" "time")
func sayHello() { fmt.Println("¡Hola desde la gorutina!")}
func main() { go sayHello() // Iniciamos la gorutina time.Sleep(time.Millisecond * 100) // Damos tiempo a la gorutina para ejecutarse fmt.Println("Función principal finalizada")}Ten en cuenta: si no añades time.Sleep, el programa podría finalizar antes de que la gorutina comience a ejecutarse. Para la sincronización es mejor usar otros mecanismos que veremos a continuación.
Canales (Channels): transferencia de datos entre gorutinas
Los canales son «tuberías» tipadas a través de las cuales las gorutinas pueden intercambiar datos. Un canal se crea con la función make y el operador chan. Los datos se envían al canal con el operador <-, y se reciben de él también con <-, pero a la izquierda del canal.
Veamos un ejemplo con canal con búfer y sin búfer:
package main
import "fmt"
func main() { // Canal sin búfer (tamaño 0) ch := make(chan string) go func() { ch <- "Mensaje desde la gorutina" // Enviamos datos }() msg := <-ch // Recibimos datos (se bloquea hasta que llegue el mensaje) fmt.Println(msg) // Canal con búfer (tamaño 2) bufferedCh := make(chan int, 2) bufferedCh <- 10 bufferedCh <- 20 fmt.Println(<-bufferedCh) // 10 fmt.Println(<-bufferedCh) // 20}Importante: Un canal sin búfer bloquea al emisor hasta que el receptor recibe los datos. Un canal con búfer bloquea al emisor solo cuando el búfer está lleno.
Select: multiplexación de canales
El operador select permite a una gorutina esperar varios canales a la vez y procesar aquel que esté listo primero. Es análogo a switch, pero para canales. select es una herramienta potente para crear tiempos de espera, cancelar operaciones y realizar comprobaciones no bloqueantes.
package main
import ( "fmt" "time")
func main() { ch1 := make(chan string) ch2 := make(chan string) // Gorutina 1: envía datos después de 1 segundo go func() { time.Sleep(1 * time.Second) ch1 <- "Datos del canal 1" }() // Gorutina 2: envía datos después de 2 segundos go func() { time.Sleep(2 * time.Second) ch2 <- "Datos del canal 2" }() select { case msg1 := <-ch1: fmt.Println("Recibido:", msg1) case msg2 := <-ch2: fmt.Println("Recibido:", msg2) case <-time.After(500 * time.Millisecond): fmt.Println("¡Tiempo de espera agotado! Ningún canal respondió en 500 ms") }}En este ejemplo, select se activará por tiempo de espera, ya que ningún canal está listo en 500 ms. Si elimináramos el tiempo de espera, el programa esperaría al primer canal listo.