Introdução: por que usar filas de tarefas em Go?
No desenvolvimento moderno de backend, frequentemente surgem tarefas que não exigem resposta imediata ao usuário: envio de email, geração de relatórios, processamento de imagens, sincronização com APIs externas. Executar essas operações de forma síncrona dentro de uma requisição HTTP é uma má prática, pois retarda a resposta e aumenta a carga no servidor.
As filas de tarefas (job queues) resolvem esse problema. Em Go (Golang), os mecanismos nativos de concorrência — goroutines e canais — permitem construir filas confiáveis e eficientes sem dependências externas. Neste artigo, vamos explorar como criar uma fila de tarefas do zero, otimizá-la com um worker pool e evitar erros comuns.
Fundamentos: goroutines e canais
Go fornece dois primitivos principais para trabalhar com filas:
- Goroutines — threads leves que são iniciadas com a palavra-chave
go. - Canais — "tubos" tipados para transferir dados entre goroutines.
A fila mais simples é uma goroutine que lê de um canal e processa tarefas:
package main
import ( "fmt" "time")
func worker(jobs <-chan int) { for job := range jobs { fmt.Printf("Processando tarefa %d", job) time.Sleep(1 * time.Second) // simulação de trabalho }}
func main() { jobs := make(chan int, 10)
// inicia o worker go worker(jobs)
// envia tarefas for i := 1; i <= 5; i++ { jobs <- i }
close(jobs) // fecha o canal após enviar todas as tarefas time.Sleep(2 * time.Second)}Este código cria um único worker que processa tarefas sequencialmente. Mas para projetos reais, é necessária escalabilidade — vários workers.
Implementação de Worker Pool em Go
Worker pool (pool de workers) é um padrão onde iniciamos um número fixo de goroutines workers, cada uma pegando tarefas de um canal compartilhado. Isso permite controlar o grau de paralelismo e a carga no sistema.
Implementação básica
package main
import ( "fmt" "sync" "time")
func worker(id int, jobs <-chan int, wg *sync.WaitGroup) { defer wg.Done() for job := range jobs { fmt.Printf("Worker %d: tarefa %d", id, job) time.Sleep(500 * time.Millisecond) }}
func main() { const numWorkers = 3 const numJobs = 10
jobs := make(chan int, numJobs) var wg sync.WaitGroup
// inicia os workers for i := 1; i <= numWorkers; i++ { wg.Add(1) go worker(i, jobs, &wg) }
// envia tarefas for j := 1; j <= numJobs; j++ { jobs <- j } close(jobs)
wg.Wait() // aguarda a conclusão de todos os workers fmt.Println("Todas as tarefas concluídas")}Aqui usamos sync.WaitGroup para aguardar a conclusão de todos os workers. O canal com buffer numJobs permite enviar todas as tarefas sem bloqueio, enquanto os workers ainda não as pegaram.
Adição dinâmica de tarefas
Em aplicações reais, as tarefas chegam assincronamente — por exemplo, via HTTP. Aqui está um exemplo com um handler:
package main
import ( "fmt" "net/http" "sync" "time")
type Task struct { ID int Data string}
var ( taskQueue = make(chan Task, 100) wg sync.WaitGroup)
func worker(id int) { defer wg.Done() for task := range taskQueue { fmt.Printf("[Worker %d] Processando: %s (ID=%d)", id, task.Data, task.ID) time.Sleep(1 * time.Second) }}
func handler(w http.ResponseWriter, r *http.Request) { // obtém a tarefa da requisição (simplificado) task := Task{ID: time.Now().Nanosecond(), Data: "Nova tarefa"} taskQueue <- task w.W