Working with Task Queues in Go: For Beginners
Task queues are one of the fundamental patterns in modern development. In Go (Golang), this mechanism is implemented particularly elegantly thanks to built-in concurrency primitives: goroutines and channels. In this article, we will break down how to create task queues in Go, starting with simple examples and moving to advanced techniques.
What is a Task Queue and Why Do You Need It?
A task queue is a data structure that operates on the FIFO principle (First In, First Out). Imagine a queue at a checkout: the first customer is served, then the second, and so on. In programming, this allows you to:
- Separate task submission from task processing
- Limit the number of simultaneously executing operations
- Distribute load evenly among handlers
- Process tasks in the background without blocking the main thread
Go is ideally suited for implementing queues thanks to its concurrency model. Unlike many languages that require an external library (e.g., RabbitMQ or Redis), in Go you can create a simple task queue with just a few lines of code.
Creating a Simple Task Queue with Go Channels
The simplest way to implement a task queue is to use a buffered channel. Let's look at a basic example:
package main
import ( "fmt" "time")
func worker(tasks <-chan string, id int) { for task := range tasks { fmt.Printf("Worker %d started task: %s", id, task) time.Sleep(1 * time.Second) // Simulating work fmt.Printf("Worker %d completed task: %s", id, task) }}
func main() { tasks := make(chan string, 5) // Buffered channel for 5 tasks
// Start 2 workers for i := 1; i <= 2; i++ { go worker(tasks, i) }
// Send tasks to the queue for i := 1; i <= 10; i++ { tasks <- fmt.Sprintf("Task #%d", i) }
close(tasks) // Close the channel after sending all tasks time.Sleep(2 * time.Second) // Allow time for completion}In this example:
- The
taskschannel acts as the queue - Two workers (goroutines) constantly listen to the channel and process tasks
- A buffer size of 5 allows sending up to 5 tasks without immediate processing
Worker Pool Pattern: Scalable Processing
A Worker Pool is an evolution of a simple queue. Instead of a fixed number of workers, we create a pool that can be dynamically scaled. This pattern is especially useful for:
- Processing HTTP requests
- Batch data processing
- Working with external APIs (limiting the number of concurrent requests)
Here is a more advanced implementation using a struct:
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 executed task %d: %s", id, task.ID, task.Payload) time.Sleep(500 * time.Millisecond) // Simulating work wp.results <- result }}
func (wp *WorkerPool) AddTask(task Task) { wp.tasks <- task}\