Go Lesson 40 – concurrency Patterns | Dataplexa

Concurrency Patterns in Go

In real-world Go applications, concurrency is not just about using goroutines. It is about structuring them correctly using proven concurrency patterns.

In this lesson, you will learn the most important concurrency patterns used in production Go systems such as APIs, background workers, data pipelines, and distributed services.


What Are Concurrency Patterns?

Concurrency patterns are reusable designs that solve common problems related to:

  • Task coordination
  • Parallel execution
  • Cancellation and timeouts
  • Data sharing between goroutines

Go encourages simple, readable concurrency patterns using goroutines, channels, and select statements.


Why Concurrency Patterns Matter

Poorly designed concurrency can lead to:

  • Deadlocks
  • Race conditions
  • Memory leaks
  • Unpredictable behavior

Using standard patterns helps you build safe, maintainable, and scalable systems.


Pattern 1: Fan-Out / Fan-In

This pattern distributes work to multiple goroutines (fan-out) and collects results into a single channel (fan-in).

Use Case

Processing a large dataset in parallel.

func worker(id int, jobs <-chan int, results chan<- int) {
    for job := range jobs {
        results <- job * 2
    }
}
jobs := make(chan int, 5)
results := make(chan int, 5)

for i := 1; i <= 3; i++ {
    go worker(i, jobs, results)
}

for j := 1; j <= 5; j++ {
    jobs <- j
}
close(jobs)

for a := 1; a <= 5; a++ {
    fmt.Println(<-results)
}

Multiple workers process jobs concurrently, and results are collected safely.


Pattern 2: Pipeline

A pipeline breaks a task into stages, where each stage runs in its own goroutine and passes data to the next stage.

Use Case

Data processing pipelines (ETL systems).

func generate(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}
func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}
for result := range square(generate(1, 2, 3, 4)) {
    fmt.Println(result)
}

Each stage is isolated and easy to test.


Pattern 3: Worker Pool

Worker pools limit the number of concurrent goroutines processing tasks.

Use Case

Handling API requests or background jobs.

const workers = 3
jobs := make(chan int)
results := make(chan int)

for w := 1; w <= workers; w++ {
    go worker(w, jobs, results)
}

This prevents system overload by controlling concurrency.


Pattern 4: Cancellation with Context

Goroutines must stop work when cancellation is requested.

select {
case <-ctx.Done():
    return
case data := <-input:
    process(data)
}

This pattern is essential for graceful shutdown.


Pattern 5: Timeout Pattern

Prevent blocking forever by enforcing time limits.

select {
case res := <-ch:
    fmt.Println(res)
case <-time.After(2 * time.Second):
    fmt.Println("Timeout")
}

Pattern 6: Select Multiplexing

The select statement allows waiting on multiple channels.

select {
case msg1 := <-ch1:
    fmt.Println(msg1)
case msg2 := <-ch2:
    fmt.Println(msg2)
default:
    fmt.Println("No data available")
}

Pattern 7: Graceful Shutdown

Graceful shutdown ensures all goroutines complete before program exit.

signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)

<-signalChan
fmt.Println("Shutting down...")

Real-World Example

Modern Go servers combine multiple patterns:

  • Worker pools for request handling
  • Pipelines for data processing
  • Context cancellation for timeouts
  • Graceful shutdown on signals

These patterns together create robust production systems.


Common Mistakes

  • Launching unlimited goroutines
  • Ignoring cancellation signals
  • Blocking on channels without timeouts
  • Sharing mutable state without protection

Practice Exercises

Exercise 1

Implement a worker pool that processes numbers and returns their squares.

Exercise 2

Build a pipeline with three stages and observe data flow.


Key Takeaways

  • Concurrency patterns improve reliability
  • Go provides simple primitives to implement them
  • Patterns reduce bugs and complexity
  • Used extensively in production systems

What’s Next?

In the next lesson, you will move into building APIs with Go and apply concurrency patterns in real projects.