Skip to content

Tutorial: Your First Job in Go

This tutorial walks you through building a background job system with the Go SDK. You will enqueue, process, and monitor a job using Go — no JavaScript required.

  • Docker and Docker Compose
  • Go 1.22 or later

If you haven’t already, create a docker-compose.yml:

services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
ojs-server:
image: ghcr.io/openjobspec/ojs-backend-redis:latest
ports:
- "8080:8080"
environment:
REDIS_URL: redis://redis:6379
depends_on:
- redis
Terminal window
docker compose up -d
Terminal window
mkdir ojs-go-tutorial && cd ojs-go-tutorial
go mod init ojs-go-tutorial
go get github.com/openjobspec/ojs-go-sdk

Create main.go:

main.go
package main
import (
"context"
"fmt"
"log"
"github.com/openjobspec/ojs-go-sdk/client"
)
func main() {
ctx := context.Background()
// Create a client pointing to the OJS server
c, err := client.New("http://localhost:8080")
if err != nil {
log.Fatal(err)
}
// Enqueue a job of type "email.send" on the "default" queue
job, err := c.Enqueue(ctx, "email.send",
client.WithArgs("user@example.com", "welcome"),
client.WithQueue("default"),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Enqueued job %s in state: %s\n", job.ID, job.State)
}

Run it:

Terminal window
go run main.go

You should see:

Enqueued job 019461a8-1a2b-7c3d-8e4f-5a6b7c8d9e0f in state: available

Create worker/main.go:

worker/main.go
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/openjobspec/ojs-go-sdk/worker"
)
func main() {
// Create a worker that polls the "default" queue
w, err := worker.New("http://localhost:8080",
worker.WithQueues("default"),
worker.WithConcurrency(5),
)
if err != nil {
log.Fatal(err)
}
// Register a handler for "email.send" jobs
w.Handle("email.send", func(ctx worker.JobContext) error {
to := ctx.Args[0].(string)
template := ctx.Args[1].(string)
fmt.Printf("Sending %q email to %s\n", template, to)
// Your email logic goes here
return nil
})
// Graceful shutdown on Ctrl+C
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-stop
fmt.Println("\nShutting down worker...")
w.Stop(context.Background())
}()
fmt.Println("Worker started, waiting for jobs...")
if err := w.Start(context.Background()); err != nil {
log.Fatal(err)
}
}

Run the worker:

Terminal window
go run worker/main.go

Output:

Worker started, waiting for jobs...
Sending "welcome" email to user@example.com

Modify the enqueue call to add a retry policy:

job, err := c.Enqueue(ctx, "email.send",
client.WithArgs("user@example.com", "welcome"),
client.WithQueue("default"),
client.WithRetry(client.RetryPolicy{
MaxAttempts: 5,
Backoff: "exponential",
}),
)

If the worker handler returns an error, the job transitions to retryable and is automatically rescheduled with exponential backoff.

Add logging and recovery middleware to the worker:

import "github.com/openjobspec/ojs-go-sdk/middleware"
w, err := worker.New("http://localhost:8080",
worker.WithQueues("default"),
worker.WithConcurrency(5),
worker.WithMiddleware(
middleware.Recovery(),
middleware.Logging(slog.Default()),
),
)

This logs every job execution and recovers from panics in handlers.

  • A Go client that enqueues jobs to an OJS server
  • A Go worker that processes jobs with concurrency and graceful shutdown
  • Retry policies for automatic failure recovery
  • Middleware for cross-cutting concerns