Fan-In and Fan-Out in Go: Parallelism Made Simple

Learn how to distribute work across multiple goroutines and merge their results back into a single stream using Go's Fan-Out and Fan-In patterns. We'll break down these powerful concurrency patterns with practical examples you can use right away.

Fan-In and Fan-Out in Go: Parallelism Made Simple

Writing concurrent programs in Go is surprisingly straightforward once you get the hang of goroutines and channels. But when you're juggling multiple tasks at once, two patterns keep showing up: Fan-Out and Fan-In. These patterns let you run tasks in parallel and bring their results back together without your code turning into spaghetti.

Let me walk you through both concepts with some diagrams and examples that actually make sense.

Hi there! I'm Lince. I'm currently building a private AI code review tool that runs on your own LLM key (OpenAI, Gemini, you name it) with flat, no-seat pricing — perfect for small teams. If that sounds useful, check it out!

Fan-Out: Spreading Work Across Goroutines

Fan-Out is when you have one input channel feeding tasks to multiple worker goroutines. Each worker grabs tasks from that same channel and handles them on its own.

Picture a bakery counter: there's one line of customers, but several bakers working. Each baker serves whoever's next in line.

[Tasks Channel]
          |
    -----------------
    |       |       |
 Worker1 Worker2 Worker3

A Quick Demo of Fan-Out

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        fmt.Printf("Worker %d processing job %d\n", id, job)
        time.Sleep(time.Second) // simulate work
    }
}

func main() {
    jobs := make(chan int, 5)
    var wg sync.WaitGroup
    
    // Start 3 workers
    for w := 1; w <= 3; w++ {
        wg.Add(1)
        go worker(w, jobs, &wg)
    }
    
    // Send jobs
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)
    
    wg.Wait()
}

Three workers pull jobs from the jobs channel. The work gets distributed automatically—that's the fan-out in action.

Fan-In: Collecting Results Back

Fan-In is the opposite: you have multiple goroutines churning out results, and you funnel all those results into one channel. This way, you can collect everything into a single stream.

Think of it like a newsroom where multiple reporters file their stories to one editor's inbox.

Worker1  Worker2  Worker3
    |        |        |
    --------------------
           |
     [Results Channel]

Here's How Fan-In Works in Code

package main

import (
    "fmt"
    "time"
)

func worker(id int, results chan<- string) {
    time.Sleep(time.Duration(id) * time.Second)
    results <- fmt.Sprintf("Worker %d finished", id)
}

func main() {
    results := make(chan string, 3)
    
    go worker(1, results)
    go worker(2, results)
    go worker(3, results)
    
    for i := 0; i < 3; i++ {
        fmt.Println(<-results)
    }
}

Three workers finish at different times, but they all send their messages to the same results channel.

Putting It Together: Fan-Out + Fan-In

More often than not, you'll use both patterns together:

  • Fan-Out: split work across multiple workers
  • Fan-In: collect their results in one place

Example: Processing Tasks in Parallel

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- string, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
        results <- fmt.Sprintf("Worker %d finished job %d", id, job)
    }
}

func main() {
    rand.Seed(time.Now().UnixNano())
    
    jobs := make(chan int, 10)
    results := make(chan string, 10)
    var wg sync.WaitGroup
    
    // Start 3 workers (Fan-Out)
    for w := 1; w <= 3; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }
    
    // Send 5 jobs
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)
    
    // Wait for all workers to finish, then close results
    go func() {
        wg.Wait()
        close(results)
    }()
    
    // Collect results (Fan-In)
    for res := range results {
        fmt.Println(res)
    }
}

Here's the breakdown:

  • Jobs get spread across workers (Fan-Out)
  • Workers send their results to one channel (Fan-In)
  • The main function collects and prints everything

Why This Actually Matters

These patterns make your Go programs:

  • Faster: Work gets divided among goroutines instead of running sequentially
  • Scalable: Need to handle more load? Just spin up more workers
  • Cleaner: Channels keep your concurrency logic readable

Whenever you need to parallelize work and gather the results, Fan-Out and Fan-In are your best friends. They're not just theoretical patterns—they're practical tools you'll reach for constantly in real Go development.