Programación asíncrona en Go: guía completa para principiantes
Go (Golang) fue creado teniendo en cuenta las necesidades del desarrollo moderno, donde el rendimiento y el paralelismo juegan un papel clave. A diferencia de muchos lenguajes, donde la asincronía se implementa mediante callbacks o promesas, Go ofrece mecanismos integrados y elegantes: gorutinas y canales. Esta guía te presentará los fundamentos de la programación asíncrona en Go y te mostrará cómo escribir código concurrente eficiente.
¿Qué son las gorutinas y en qué se diferencian de los hilos?
Una gorutina es un hilo de ejecución ligero, gestionado por el runtime de Go. A diferencia de los hilos del sistema operativo, las gorutinas consumen muchos menos recursos: su pila comienza con unos pocos kilobytes y puede crecer y reducirse dinámicamente. Esto permite ejecutar millones de gorutinas simultáneamente sin una carga significativa en la memoria.
Iniciar una gorutina es muy sencillo: basta con añadir la palabra clave go antes de la llamada a la función:
package main
import ( "fmt" "time")
func sayHello() { fmt.Println("¡Hola desde una gorutina!")}
func main() { go sayHello() // Inicio de la gorutina time.Sleep(100 * time.Millisecond) // Esperamos para que la gorutina tenga tiempo de ejecutarse fmt.Println("Función principal finalizada")}Es importante destacar: si la función principal main() finaliza, todas las gorutinas iniciadas serán detenidas forzosamente. Para la sincronización utilizamos herramientas especiales.
Sincronización con WaitGroup
La forma más sencilla de esperar a que finalicen las gorutinas es usar sync.WaitGroup. Funciona como un contador: lo incrementamos al iniciar una gorutina y lo decrementamos al finalizar. La función principal se bloquea hasta que el contador llega a cero.
package main
import ( "fmt" "sync" "time")
func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // Decrementamos el contador al finalizar fmt.Printf("Trabajador %d comenzó a trabajar", id) time.Sleep(time.Second) fmt.Printf("Trabajador %d terminó de trabajar", id)}
func main() { var wg sync.WaitGroup
for i := 1; i <= 3; i++ { wg.Add(1) // Incrementamos el contador antes de iniciar go worker(i, &wg) }
wg.Wait() // Esperamos a que todas las gorutinas finalicen fmt.Println("Todos los trabajadores han terminado su trabajo")}Observa: wg.Add(1) se llama antes de iniciar la gorutina, y wg.Done() dentro de ella (normalmente mediante defer). Esto garantiza que el contador se decremente correctamente incluso en caso de pánico.
Canales: comunicación entre gorutinas
Los canales (channels) son «tuberías» tipificadas a través de las cuales las gorutinas pueden intercambiar datos de forma segura. Los canales proporcionan sincronización por defecto: el envío a un canal bloquea la gorutina hasta que otra gorutina lea el valor, y viceversa.
Los canales se crean con la función make:
package main
import "fmt"
func main() { // Creamos un canal no buferizado para números enteros ch := make(chan int)
// Iniciamos una gorutina que envía datos go func() { ch <- 42 // Enviamos un valor al canal }()
// Leemos del canal (se bloquea hasta que llegue un valor) value := <-ch fmt.Println("Valor recibido:", value)}Los canales pueden ser:
- No buferizados — síncronos, el envío se bloquea hasta la recepción.
- Buferizados — asíncronos, el envío se bloquea solo cuando el búfer está lleno.
// Creación de un canal buferizado de 3 elementosbufferedCh := make(chan string, 3)bufferedCh <- "A"bufferedCh <- "B"bufferedCh <- "C" // Mientras no esté lleno, no bloquea