go 101 lines · 3 tabs

Fan-Out/Fan-In Pipeline With Channels and Context Cancellation in Go

Shared by codesnips Jul 2026
3 tabs
package pipeline

import "context"

func generate(ctx context.Context, nums ...int) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for _, n := range nums {
			select {
			case out <- n:
			case <-ctx.Done():
				return
			}
		}
	}()
	return out
}

func square(ctx context.Context, in <-chan int) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for n := range in {
			select {
			case out <- n * n:
			case <-ctx.Done():
				return
			}
		}
	}()
	return out
}
3 files · go Explain with highlit

This snippet builds a three-stage concurrent pipeline in Go where each stage runs in its own goroutine (or pool of goroutines) and communicates only through channels. The pipeline pattern decomposes a data-processing job into independent stages — generate, transform, collect — so each stage can run concurrently and back-pressure flows naturally through unbuffered or small-buffered channels. Because each stage reads from an input channel and writes to an output channel, stages are decoupled and composable, and the runtime schedules them across available cores.

In stages.go, generate is the source stage: it emits work items onto a channel and closes it when done, which signals completion downstream. The critical detail is the select on ctx.Done() alongside the send — without it, a blocked send on a stalled channel would leak the goroutine forever if a consumer stops reading. square is a stateless transform stage that ranges over its input until the channel is closed, then closes its own output, propagating the end-of-stream signal.

The fan-out/fan-in mechanism lives in fanin.go. fanOut starts n identical worker goroutines all reading from the same input channel, letting the runtime distribute items to whichever worker is free — a simple load-balancing worker pool. fanIn merges the multiple worker outputs back into one channel using a sync.WaitGroup to know when every source is drained, then closes the merged channel exactly once. This close-after-Wait idiom is the canonical way to avoid closing a channel twice or closing it while a sender is still active.

In main.go, context.WithTimeout provides a single cancellation signal threaded through every stage, so a timeout or an early return unwinds the whole pipeline cleanly rather than leaking goroutines. The stages are wired together as generate into fanOut into fanIn, and the final for range drives the pipeline by consuming results. The key trade-offs: unbuffered channels give tight back-pressure but more context switches, while the explicit ctx plumbing adds boilerplate but guarantees no goroutine outlives the work. This structure is worth reaching for when a CPU- or IO-bound job has clear sequential phases that benefit from parallelism within a phase.


Related snips

Share this code

Here's the card — post it anywhere.

Fan-Out/Fan-In Pipeline With Channels and Context Cancellation in Go — share card
Link copied