Introducción: ¿Por qué es necesaria la programación asíncrona en Go?
Go (Golang) fue diseñado desde el principio para trabajar eficientemente con concurrencia. A diferencia de muchos lenguajes donde la asincronía se implementa mediante complejos callbacks o promesas, Go ofrece un modelo elegante basado en goroutines y channels. Este modelo permite escribir código asíncrono que se lee como síncrono, pero se ejecuta en paralelo, maximizando la utilización de los recursos de los procesadores multinúcleo.
En esta guía, analizaremos todos los conceptos clave de la programación asíncrona en Go: desde la creación de goroutines hasta patrones complejos de sincronización utilizando channels y primitivas del paquete sync. El material será útil tanto para principiantes que recién aprenden Go como para desarrolladores experimentados que deseen sistematizar sus conocimientos.
1. Goroutines: Hilos ligeros
Una goroutine es un hilo de ejecución ligero gestionado por el runtime de Go. A diferencia de los hilos del SO, las goroutines tienen una pila diminuta (que comienza con unos pocos KB) que puede crecer y encogerse dinámicamente. Esto permite ejecutar decenas de miles de goroutines simultáneamente sin un consumo significativo de memoria.
Iniciar una goroutine
Para ejecutar una función como goroutine, basta con añadir la palabra clave go antes de la llamada a la función:
package main
import ( "fmt" "time")
func printNumbers() { for i := 1; i <= 5; i++ { fmt.Println(i) time.Sleep(100 * time.Millisecond) }}
func main() { // Ejecutamos la función como goroutine go printNumbers()
// La goroutine principal continúa su trabajo fmt.Println("La goroutine principal está funcionando")
// Damos tiempo para que se ejecute la goroutine hija time.Sleep(1 * time.Second) fmt.Println("Programa finalizado")}Importante: El programa finaliza cuando termina la función main(). Si la goroutine principal termina antes, todas las goroutines hijas se detendrán forzosamente. Por eso en el ejemplo usamos time.Sleep para esperar; en la práctica, para la sincronización se utilizan channels o sync.WaitGroup.
Funciones anónimas en goroutines
Las goroutines a menudo se crean con funciones anónimas:
go func() { fmt.Println("Ejecutándose en una goroutine")}()2. Channels: Comunicación entre goroutines
Los channels son "tubos" tipificados a través de los cuales las goroutines pueden enviar y recibir valores. Este es el mecanismo principal de sincronización e intercambio de datos en Go. Los channels proporcionan acceso seguro a los datos sin necesidad de usar bloqueos explícitos (mutexes).
Creación y uso de un channel
Un channel se crea con la función make(). El operador <- se usa para enviar y recibir datos:
package main
import "fmt"
func sum(s []int, c chan int) { total := 0 for _, v := range s { total += v } c <- total // enviamos el resultado al channel}
func main() { s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int) go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c)
x, y := <-c, <-c // recibimos datos del channel
fmt.Println(x, y, x+y)}En este ejemplo, ejecutamos dos goroutines, cada una calcula la suma de la mitad del slice. Los resultados se envían al channel, y la goroutine principal los recibe y muestra la suma total.
Channels con buffer
Por defecto, los channels no tienen buffer: el envío se bloquea hasta que el receptor esté listo para recibir el valor. Los channels con buffer tienen una cola de tamaño definido:
ch := make(chan int, 2) // channel con buffer de 2 elementosch <- 1ch <- 2// ch <- 3 // error: deadlock, buffer llenofmt.Println(<-ch) // 1fmt.Println(<-ch) // 2