Introdução às filas de tarefas em Go
\nFilas de tarefas (task queues) são um dos padrões-chave no desenvolvimento de aplicações de alta carga. Elas permitem organizar o processamento assíncrono de dados, distribuir a carga e aumentar a tolerância a falhas do sistema. Na linguagem Go, graças aos mecanismos internos de concorrência — goroutines e channels — a implementação de filas torna-se incrivelmente elegante e eficiente.
\nNeste artigo, vamos explorar como criar filas de tarefas em Go do zero. Você aprenderá o que são goroutines e channels, como organizar um worker pool e processar tarefas em paralelo. O material é voltado para desenvolvedores iniciantes familiarizados com o básico da sintaxe Go.
\n\nFundamentos: goroutines e channels
\nAntes de passar para as filas, é importante entender dois conceitos fundamentais do Go:
\n- \n
- Goroutine — um fluxo de execução leve. É iniciado com a palavra-chave
goantes da chamada de uma função. \n - Channel — um "canal" tipado para transferir dados entre goroutines. Permite sincronizar o trabalho delas. \n
Exemplo simples de goroutine e channel:
\npackage main\n\nimport "fmt"\n\nfunc main() {\n ch := make(chan string)\n\n go func() {\n ch <- "Olá da goroutine!"\n }()\n\n msg := <-ch\n fmt.Println(msg)\n}\n\nNeste exemplo, criamos um channel ch, iniciamos uma goroutine anônima que envia uma mensagem para ele, e a goroutine principal recebeu essa mensagem. É exatamente nesse mecanismo que as filas de tarefas são construídas.
Implementação de uma fila de tarefas simples
\nA fila de tarefas mais simples é um channel no qual colocamos tarefas, e uma ou mais goroutines que as processam. Vejamos um exemplo 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("Worker %d iniciou a tarefa %d\\n", id, task)\n time.Sleep(time.Second) // simulação de trabalho\n fmt.Printf("Worker %d concluiu a tarefa %d\\n", id, task)\n }\n}\n\nfunc main() {\n tasks := make(chan int, 10) // channel com buffer\n\n // inicia um worker\n go worker(1, tasks)\n\n // envia 5 tarefas\n for i := 1; i <= 5; i++ {\n tasks <- i\n }\n\n close(tasks) // fecha o channel\n time.Sleep(2 * time.Second) // dá tempo para processamento\n}\n\nAqui, worker é uma função que lê do channel tasks. O channel tem buffer (capacidade 10), então podemos enviar várias tarefas sem bloqueio. Após fechar o channel, o loop for range termina automaticamente.
Observe: usamos <-chan int — um channel somente leitura. Esta é uma boa prática que indica explicitamente que o worker não deve enviar dados para este channel.
Channels com buffer e sem buffer
\nNo exemplo acima, usamos um channel com buffer (make(chan int, 10)). A diferença entre os tipos de channels:
- \n
- Channel sem buffer — o envio é bloqueado até que o receptor leia os dados. Ideal para sincronização. \n
- Channel com buffer — o envio é bloqueado apenas quando o buffer está cheio. Permite acumular tarefas e reduzir latências. \n
Para filas de tarefas, geralmente usamos channels com buffer, para que o produtor possa enviar uma tarefa rapidamente e continuar o trabalho, sem esperar sua execução.
\n\nWorker Pool em Go
\nUm worker é bom, mas vários são melhores. Um worker pool permite processar tarefas em paralelo, utilizando todos os núcleos do processador. Veja como implementar:
\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