Las goroutines son hilos de ejecución ligeros, incorporados en la arquitectura de Go desde sus primeras versiones para lograr una concurrencia eficiente. Históricamente, la idea del hilo ligero surgió como un intento de eludir el costo de los hilos del sistema, así como por la alta demanda de aplicaciones de servidor escalables. Go fue diseñado originalmente como un lenguaje para sistemas de servidor y redes, donde millones de tareas deben ser procesadas en paralelo.
Problema: La concurrencia puede llevar rápidamente a condiciones de carrera, bloqueos y un aumento en el consumo de memoria si no se controla el ciclo de vida de las goroutines, no se considera su planificación y no se gestiona su finalización.
Solución: Las goroutines se inician a través de la palabra clave go. El trabajo de las goroutines es planeado por el planificador de Go, que utiliza el modelo M:N (M hilos del sistema operativo atienden N goroutines del lenguaje Go). Para gestionar el ciclo de vida se utilizan canales, WaitGroup, context y control de cierre de canales.
Ejemplo de código:
package main import ("fmt"; "time") func worker(id int) { fmt.Printf("Trabajador %d iniciado ", id) time.Sleep(time.Second) fmt.Printf("Trabajador %d terminado ", id) } func main() { for i := 1; i <= 3; i++ { go worker(i) } time.Sleep(2 * time.Second) }
Características clave:
Si no se espera explícitamente a una goroutine en main, ¿siempre se ejecutará?
No, la ejecución de main se completa: el proceso terminará independientemente del estado de las goroutines hijas, y no todas las tareas se completarán.
¿Es el lanzamiento de go func(...) desde un bucle una garantía de que cada goroutine obtendrá su propio valor de las variables del bucle?
No, surge el problema de captura de la variable del bucle, las goroutines pueden trabajar con el mismo valor de un slice/variable. Se debe utilizar la copia de la variable, por ejemplo, pasándola como argumento:
for i := 0; i < 3; i++ { go func(n int) { fmt.Println(n) }(i) }
¿Puede una goroutine bloquear el planificador de Go y evitar que otras se ejecuten?
Sí, si inicia un ciclo infinito o muy pesado sin puntos de cambio (por ejemplo, sin llamadas de función que esperen o yield), puede mantener el hilo del sistema operativo — aunque esto contraviene la ideología de Go sobre "multitarea cooperativa". Por ejemplo, una función pesada sin bloqueos:
func busy() { for { // No hay esperas ni llamadas bloqueantes } }
En un microservicio se lanza periódicamente una goroutine para leer de la base de datos, pero se olvida de finalizarla al cancelar la solicitud. Como resultado, quedan goroutines "colgadas" que con el tiempo llevan al consumo de toda la memoria RAM.
Pros:
Contras:
Se utiliza context para controlar la cancelación de tareas, WaitGroup para gestionar la finalización de todas las goroutines antes de detener la aplicación, y canales para la correcta transmisión de datos entre los ejecutores.
Pros:
Contras: