Introduction to Goworker — Implementing Workers with Redis
Recently, I’ve been gradually migrating a project from Ruby on Rails to Golang. Part of the reason is wanting to practice writing its rather quirky syntax to see how it feels, and another part is experiencing Golang’s power firsthand while learning how to organize code without an established framework.
Today, I’m going to introduce goworker. The reason it caught my eye is that it was the first result when searching for “go worker,” and its implementation is fully compatible with the format used by resque (a Ruby worker gem). This means I can push tasks into a queue from a Ruby on Rails application and have them processed via Golang.
How to Use It
During initialization, you need to register all workers that will be executed with goworker so that it knows which corresponding task to run when pulling from a given queue:
func init() {
settings := goworker.WorkerSettings{
URI: os.Getenv("REDIS_URL"),
Queues: []string{"worker", "queues"},
UseNumber: true,
ExitOnComplete: false, // don't complete even though queue is empty.
Concurrency: concurrency,
Connections: connections,
Namespace: "myapp" + ":",
Interval: 10.0,
}
goworker.SetSettings(settings)
}
You can define concurrency and connections according to your needs. The Namespace below provides a scope for the queue names. Interval specifies how long to wait before checking for new tasks again when the queue is empty.
How do you implement a worker? You need to implement a function and register it with goworker.
func workerFn(queue string, args ...interface{}) error {
// your job.
fmt.Println(queue, args)
}
goworker.Register("MyWorker", workerFn)
Once configured, you can use goworker.Work() to listen to Redis. This is a blocking operation, so it’s typically run on a separate server or process (you can also spawn a goroutine for it when starting the server).
func main() {
err := goworker.Work()
if err != nil {
// log your error
}
}
Next, let’s push a task into Redis to see it in action:
RPUSH myapp:queue:worker '{"class": "MyWorker", "args": [1,2,3]}
You’ll see worker and [1 2 3] printed to stdout.
You can also push tasks directly into Redis using goworker.Enqueue(&goworker.Job{}).
How It Works
When a new task is added, goworker pushes it to namespace:queue:job.Queue using RPUSH. To serialize the parameters, a JSON string is used as the payload and pushed into the queue.
// workers.go L43~47
buffer, err := json.Marshal(job.Payload)
if err != nil {
logger.Criticalf("Cant marshal payload on enqueue")
return err
}
err = conn.Send("RPUSH", fmt.Sprintf("%squeue:%s", workerSettings.Namespace, job.Queue), buffer)
if err != nil {
logger.Criticalf("Cant push to queue")
return err
}
In addition, a set is used to keep track of which queues are currently in use.
// workers.go L49~53
err = conn.Send("SADD", fmt.Sprintf("%squeues", workerSettings.Namespace), job.Queue)
if err != nil {
logger.Criticalf("Cant register queue to list of use queues")
return err
}
When calling goworker.Work(), it internally invokes a poller. This poller mainly performs a few tasks:
- Retrieves tasks by calling
LPOPviapoller.getJob()
reply, err := conn.Do("LPOP", fmt.Sprintf("%squeue:%s", workerSettings.Namespace, queue))
- Periodically retrieves
jobsto be executed from a jobs channel viapoller.poll(duration, quit), usingquitas an exit signal.
jobs := make(chan *Job)
//......
return jobs
- Finally, executes
worker.work(jobs, &monitor). When a job is received, it runsw.run(job, workerFunc), which is the function we initially defined. If a matching class cannot be found, it prints the errorNo Worker for ... queue with args ....
Caveats
- Using goworker does not guarantee that pending jobs will run successfully if an unexpected shutdown occurs.
- As far as I understand, Redis doesn’t have a built-in mechanism to guarantee that the receiving end will definitely receive the message. If you need stronger reliability guarantees, protocols like AMQP might be necessary. However, for a small project, Redis is more than enough.
Summary
For general applications, Redis is quite sufficient. Unless you are dealing with genuinely massive traffic and high-concurrency scenarios, Redis-based queues are far from inadequate. In this post, we looked at developing with the goworker library paired with Redis, which should be well-suited for handling time-consuming (or delayed) background tasks. For larger-scale applications, more reliable protocols like AMQP can be adopted.
Also, my computer fans kept spinning constantly whenever I ran goworker. I’m not sure if that’s normal, though the CPU usage didn’t show any obvious spikes…
Related Posts
- When a Measure Becomes a Target: From the Window Tax to Pull Request Counts I once wrote a script to tally how many PRs I contributed in a quarter, how many reviews I left, and how many tickets I closed, hoping to use numbers to prove my output to my manager. My manager simply remarked that performance isn't just about output. Years later, I finally understood—when a measure becomes a target, it ceases to be a good measure. From the British window tax and the Hanoi rat bounty to evaluating developers by PR counts today, the underlying mechanism is exactly the same.
- Using Cloudflare Images for Image Storage and Transformation Putting an image on a webpage is the simplest task in frontend development. But doing it properly—including resizing, generating multiple formats, and withstanding heavy traffic—is actually an entire end-to-end solution. Eventually, I offloaded everything to Cloudflare Images, keeping only a single original image.
- Stop Using AWS Access Keys Access Keys are an easily overlooked security risk in AWS. By pairing OIDC with IAM Roles, GitHub Actions can securely operate AWS resources without storing any secrets.
- Database Primary Keys: AUTO_INCREMENT, UUID, and UUIDv7 Backend developers often face the choice of primary keys: should you use auto-increment or UUID? What about collisions? How does UUIDv7 compare to created_at + index in performance? Here are the design decisions and benchmark results from testing 20 million rows.