Trabajo con colas de tareas en Go: para principiantes
Las colas de tareas (task queues) son uno de los patrones fundamentales en el desarrollo moderno. En Go (Golang), este mecanismo se implementa de manera especialmente elegante gracias a los primitivos de concurrencia integrados: gorutinas y canales. En este artículo, analizaremos cómo crear colas de tareas en Go, comenzando con ejemplos simples y terminando con técnicas avanzadas.
¿Qué es una cola de tareas y para qué sirve?
Una cola de tareas es una estructura de datos que funciona bajo el principio FIFO (First In, First Out — «primero en entrar, primero en salir»). Imagínese una cola en la caja: el primer cliente es atendido, luego el segundo y así sucesivamente. En programación, esto permite:
- Separar la recepción de tareas y su procesamiento
- Limitar la cantidad de operaciones que se ejecutan simultáneamente
- Distribuir uniformemente la carga entre los procesadores
- Procesar tareas en segundo plano sin bloquear el hilo principal
Go es ideal para implementar colas gracias a su modelo de concurrencia. A diferencia de muchos lenguajes donde se requiere una biblioteca externa (por ejemplo, RabbitMQ o Redis), en Go se puede crear una cola de tareas simple con solo unas pocas líneas de código.
Creación de una cola de tareas simple con canales de Go
La forma más sencilla de implementar una cola de tareas es usar un canal con búfer. Veamos un ejemplo básico:
package main
import ( "fmt" "time")
func worker(tasks <-chan string, id int) { for task := range tasks { fmt.Printf("Worker %d inició la tarea: %s", id, task) time.Sleep(1 * time.Second) // Simulación de trabajo fmt.Printf("Worker %d completó la tarea: %s", id, task) }}
func main() { tasks := make(chan string, 5) // Canal con búfer para 5 tareas
// Iniciamos 2 workers for i := 1; i <= 2; i++ { go worker(tasks, i) }
// Enviamos tareas a la cola for i := 1; i <= 10; i++ { tasks <- fmt.Sprintf("Tarea nº%d", i) }
close(tasks) // Cerramos el canal después de enviar todas las tareas time.Sleep(2 * time.Second) // Damos tiempo para que terminenEn este ejemplo:
- El canal
tasksactúa como cola - Dos workers (gorutinas) escuchan constantemente el canal y procesan las tareas
- El búfer de tamaño 5 permite enviar hasta 5 tareas sin procesamiento inmediato
Patrón Worker Pool: procesamiento escalable
Worker Pool (grupo de workers) es una evolución de la cola simple. En lugar de un número fijo de workers, creamos un grupo que se puede escalar dinámicamente. Este patrón es especialmente útil para:
- Procesamiento de solicitudes HTTP
- Procesamiento por lotes de datos
- Trabajo con APIs externas (limitación de solicitudes simultáneas)
Aquí hay una implementación más avanzada usando una estructura:
package main
import ( "fmt" "sync" "time")
type Task struct { ID int Payload string}
type WorkerPool struct { tasks chan Task results chan string wg sync.WaitGroup workers int}
func NewWorkerPool(workers int) *WorkerPool { return &WorkerPool{ tasks: make(chan Task, 100), results: make(chan string, 100), workers: workers, }}
func (wp *WorkerPool) Start() { for i := 1; i <= wp.workers; i++ { wp.wg.Add(1) go wp.worker(i) }}
func (wp *WorkerPool) worker(id int) { defer wp.wg.Done() for task := range wp.tasks { result := fmt.Sprintf("Worker %d ejecutó la tarea %d: %s", id, task.ID, task.Payload) time.Sleep(500 * time.Millisecond) // Simulación de trabajo wp.results <- result }}
func (wp *WorkerPool) AddTask(task Task) { wp.tasks <- task}\