Introducción a las colas de tareas en Go
\nLas colas de tareas (task queues) son uno de los patrones clave en el desarrollo de aplicaciones de alta carga. Permiten organizar el procesamiento asíncrono de datos, distribuir la carga y aumentar la tolerancia a fallos del sistema. En el lenguaje Go, gracias a los mecanismos integrados de concurrencia — gorutinas y canales — la implementación de colas se vuelve increíblemente elegante y eficiente.
\nEn este artículo, analizaremos cómo crear colas de tareas en Go desde cero. Aprenderá qué son las gorutinas y los canales, cómo organizar un pool de trabajadores (worker pool) y procesar tareas en paralelo. El material está dirigido a desarrolladores principiantes familiarizados con los conceptos básicos de la sintaxis de Go.
\n\nFundamentos: gorutinas y canales
\nAntes de pasar a las colas, es importante comprender dos conceptos fundamentales de Go:
\n- \n
- Gorutina (goroutine) — un hilo de ejecución ligero. Se inicia con la palabra clave
goantes de la llamada a una función. \n - Canal (channel) — un «canal» tipado para transferir datos entre gorutinas. Permite sincronizar su trabajo. \n
Ejemplo de una gorutina y un canal simples:
\npackage main\n\nimport "fmt"\n\nfunc main() {\n ch := make(chan string)\n\n go func() {\n ch <- "¡Saludo desde la gorutina!"\n }()\n\n msg := <-ch\n fmt.Println(msg)\n}\n\nEn este ejemplo, creamos un canal ch, iniciamos una gorutina anónima que envía un mensaje a él, y la gorutina principal recibió ese mensaje. Este mecanismo es la base sobre la que se construyen las colas de tareas.
Implementación de una cola de tareas simple
\nLa cola de tareas más simple es un canal en el que colocamos tareas, y una o varias gorutinas que las procesan. Veamos un ejemplo básico:
\npackage main\n\nimport (\n "fmt"\n "time"\n)\n\nfunc worker(id int, tasks <-chan int) {\n for task := range tasks {\n fmt.Printf("Trabajador %d inició tarea %d\\n", id, task)\n time.Sleep(time.Second) // simulación de trabajo\n fmt.Printf("Trabajador %d completó tarea %d\\n", id, task)\n }\n}\n\nfunc main() {\n tasks := make(chan int, 10) // canal con buffer\n\n // iniciamos un trabajador\n go worker(1, tasks)\n\n // enviamos 5 tareas\n for i := 1; i <= 5; i++ {\n tasks <- i\n }\n\n close(tasks) // cerramos el canal\n time.Sleep(2 * time.Second) // damos tiempo para procesar\n}\n\nAquí, worker es una función que lee del canal tasks. El canal tiene buffer (capacidad 10), por lo que podemos enviar varias tareas sin bloqueo. Después de cerrar el canal, el bucle for range finaliza automáticamente.
Tenga en cuenta: usamos <-chan int — un canal de solo lectura. Esta es una buena práctica que indica explícitamente que el trabajador no debe enviar datos a este canal.
Canales con buffer y sin buffer
\nEn el ejemplo anterior, usamos un canal con buffer (make(chan int, 10)). La diferencia entre los tipos de canales:
- \n
- Canal sin buffer — el envío se bloquea hasta que el receptor lea los datos. Ideal para sincronización. \n
- Canal con buffer — el envío se bloquea solo cuando el buffer está lleno. Permite acumular tareas y reduce las demoras. \n
Para las colas de tareas, generalmente se usan canales con buffer, para que el productor pueda enviar una tarea rápidamente y continuar su trabajo sin esperar a que se ejecute.
\n\nPool de trabajadores (Worker Pool) en Go
\nUn trabajador está bien, pero varios es mejor. Un pool de trabajadores permite procesar tareas en paralelo, utilizando todos los núcleos del procesador. Así es como se implementa:
\npackage main\n\nimport (\n "fmt"\n "sync"\n "time"\n)\n\nfunc worker(id int, tasks <-chan int, wg *sync.WaitGroup) {\n defer wg.Done()\n fo