Skip to content
Harjot Singh Rana
Back to blog
GoConcurrencyBackend6 min read

Go Concurrency Patterns for Production Systems

July 12, 2024

Why Go for backend services

Go's concurrency model — goroutines and channels — is one of the cleanest abstractions for building high-throughput backend services. It's not just about performance; it's about clarity. A well-written concurrent Go program is easier to reason about than its callback-heavy Node.js or thread-pool Java equivalent.

Worker pool pattern

The worker pool is the bread and butter of concurrent Go. Fixed number of workers, a shared work channel, and results collected via a results channel:

func WorkerPool(ctx context.Context, jobs []Job, workers int) []Result {
    jobCh := make(chan Job, len(jobs))
    resultCh := make(chan Result, len(jobs))

    // Start workers
    for i := 0; i < workers; i++ {
        go worker(ctx, jobCh, resultCh)
    }

    // Send jobs
    for _, job := range jobs {
        jobCh <- job
    }
    close(jobCh)

    // Collect results
    var results []Result
    for i := 0; i < len(jobs); i++ {
        results = append(results, <-resultCh)
    }
    return results
}

Fan-out, fan-in

Fan-out distributes work across multiple goroutines. Fan-in merges multiple result channels into one. This pattern is perfect for parallelizing independent work like processing webhook deliveries or batch database operations.

Graceful shutdown

A production service must handle SIGTERM gracefully. Use signal.Notify with a context cancellation to drain in-flight work:

func main() {
    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
    defer stop()

    server := &http.Server{Addr: ":8080"}
    go server.ListenAndServe()

    <-ctx.Done()
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    server.Shutdown(shutdownCtx)
}

Things I've learned the hard way

  • Always set a context timeout on external calls
  • Buffered channels are not a substitute for backpressure
  • Use errgroup for coordinating goroutines that can fail
  • Profile before optimizing — most bottlenecks are not where you think
Built with Moonshift