Introdução: por que precisamos de filas de tarefas em Go?
\nNo desenvolvimento moderno, frequentemente surge a necessidade de processar tarefas de forma assíncrona, sem bloquear o fluxo principal da aplicação. Filas de tarefas (task queues) são um padrão clássico que permite distribuir o trabalho entre vários processadores (workers), gerenciar a carga e garantir tolerância a falhas.
\nA linguagem Go (Golang) fornece mecanismos integrados para implementar filas de tarefas: goroutines e canais (channels). Neste artigo, vamos analisar na prática como construir uma fila de tarefas confiável usando a biblioteca padrão do Go, sem recorrer a brokers de mensagens externos (embora em produção RabbitMQ ou Redis possam ser úteis).
\n\nParte Principal
\n\n1. Padrão Básico de Worker Pool
\nWorker Pool é um conjunto de um número fixo de goroutines (workers) que escutam um único canal e processam as tarefas recebidas. Isso permite controlar o grau de paralelismo e evitar sobrecarga do sistema.
\nVamos considerar um exemplo simples: criamos um pool de 3 workers que processam números inteiros (simulação de trabalho).
\npackage main\n\nimport (\n "fmt"\n "sync"\n "time"\n)\n\nfunc worker(id int, jobs <-chan int, wg *sync.WaitGroup) {\n defer wg.Done()\n for job := range jobs {\n fmt.Printf("Worker %d iniciou a tarefa %d\\n", id, job)\n time.Sleep(time.Second) // simulação de trabalho\n fmt.Printf("Worker %d concluiu a tarefa %d\\n", id, job)\n }\n}\n\nfunc main() {\n const numJobs = 5\n const numWorkers = 3\n\n jobs := make(chan int, numJobs)\n var wg sync.WaitGroup\n\n // Iniciando os workers\n for w := 1; w <= numWorkers; w++ {\n wg.Add(1)\n go worker(w, jobs, &wg)\n }\n\n // Enviando tarefas\n for j := 1; j <= numJobs; j++ {\n jobs <- j\n }\n close(jobs) // fechando o canal para que os workers terminem\n\n wg.Wait()\n fmt.Println("Todas as tarefas concluídas")\n}\nAnálise do código:
\n- \n
jobs— canal com buffer para onde enviamos as tarefas. \n worker— função que lê do canal e processa cada tarefa. \n sync.WaitGroup— aguarda a conclusão de todos os workers. \n - Após enviar todas as tarefas, o canal é fechado (
close(jobs)), o que faz com que o loopfor rangenos workers termine. \n
Este padrão é a base para soluções mais complexas.
\n\n2. Fila com Prioridades e Timeouts
\nEm aplicações reais, as tarefas podem ter prioridades diferentes ou exigir execução dentro de um determinado tempo. Vamos implementar uma fila onde tarefas com prioridade são processadas primeiro, e também adicionar um contexto com timeout para cancelar tarefas longas.
\npackage main\n\nimport (\n "context"\n "fmt"\n "time"\n)\n\ntype Task struct {\n ID int\n Priority int // quanto menor o número, maior a prioridade\n Data string\n}\n\nfunc processTask(ctx context.Context, task Task) error {\n select {\n case <-ctx.Done():\n return ctx.Err()\n case <-time.After(time.Duration(task.Priority) * 500 * time.Millisecond):\n fmt.Printf("Tarefa processada %d: %s (prioridade %d)\\n", task.ID, task.Data, task.Priority)\n return nil\n }\n}\n\nfunc main() {\n tasks := []Task{\n {ID: 1, Priority: 3, Data: "Baixa prioridade"},\n {ID: 2, Priority: 1, Data: "Alta prioridade"},\n {ID: 3, Priority: 2, Data: "Prioridade média"},\n }\n\n // Ordenação por prioridade (as mais altas primeiro)\n // Em um projeto real, use ordenação ou heap (container/heap)\n for i := 0; i < len(tasks); i++ {\n for j := i + 1; j < len(tasks); j++ {\n if tasks[j].Priority < tasks[i].Priority {\n tasks[i], tasks[j] = tasks[j], tasks[i]\n }\n