Fan-in
In the Coordination chapter you merged two channels with a select loop,
disabling each case as its channel closed. That works beautifully for two. For
three, it's clumsy; for a slice of channels whose length you don't know until
runtime, select can't help at all — you can't write a case per channel when
the count is variable. Fan-in is the pattern that scales: merge any
number of input channels into one output.
A goroutine per input
The trick is to stop trying to watch all inputs from one loop. Instead, give each input its own goroutine that copies its values to the shared output:
func fanIn(inputs ...<-chan int) <-chan int {
out := make(chan int)
for _, in := range inputs {
go func(c <-chan int) {
for v := range c { // drain this one input
out <- v // forward onto the shared output
}
}(in)
}
return out
}
Each forwarding goroutine ranges over one input until it closes, then returns.
Values from all inputs land on out, interleaved in whatever order they arrive.
Note in is passed as an argument — capturing the loop variable by reference is
a classic fan-in bug.
Who closes the output?
The code above has a fatal flaw: nobody closes out. A consumer doing range out would forward every value and then block forever, waiting for more. As
with the worker pool, the answer is close after everyone is done — and again,
sync.WaitGroup is how you know:
func fanIn(inputs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
wg.Add(len(inputs))
for _, in := range inputs {
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(in)
}
go func() {
wg.Wait()
close(out) // every forwarder has finished; safe to close
}()
return out
}
wg.Add(len(inputs)) counts one per forwarder. Each Dones when its input
drains. A separate goroutine waits for the whole group, then closes out. The
close must live in its own goroutine: if you called wg.Wait() before
return, fanIn would block before the caller could ever receive, and since
out is unbuffered the forwarders would be stuck too. This is the same
WaitGroup-then-close idiom as the worker pool — here it drives the whole
pattern.
The zero-channel case falls out for free
Call fanIn() with no inputs and the loop never runs, so wg starts at zero,
wg.Wait() returns immediately, and out is closed right away. A range over
it ends at once — no special-casing needed. That's a good sign the structure is
right: the empty case isn't an exception, it's just len(inputs) == 0 flowing
through the same code.
Fan-in and fan-out together
Fan-in is the natural partner of the worker pool (which fans out work to many
workers). A common pipeline fans out — several workers each producing a result
channel — then fans those channels back in to a single stream a downstream
stage can range over. Because fan-in preserves no ordering (values race onto
out as they're produced), use it when you care that every value arrives, not
when you care about the order they arrive in.
Your exercise: build fanIn for <-chan int. Get the WaitGroup-then-close
right, and it will merge two channels or two hundred with the same few lines.
Practice
1. Fan-in
AdvancedImplement
fanInso it merges any number of input channels into one:func fanIn(chans ...<-chan int) <-chan int- Start one goroutine per input channel. Each ranges over its input and forwards every value onto a single shared output channel.
- The returned channel must be closed once all inputs are drained and
closed — otherwise a
rangeover it blocks forever. Use async.WaitGroupto learn when every forwarder has finished, then close the output. The close must run in its own goroutine sofanIncan return immediately. - Order is not preserved — values from different inputs may interleave any way. The tests sort before comparing.
fanIn()with no inputs must return a channel that is closed right away, so ranging over it ends immediately.
This is the general case of the two-channel
selectmerge: it works for two channels or two hundred with the same code.package main import "fmt" // fanIn merges every value from every input channel onto a single output // channel, closing it once all inputs are drained and closed. func fanIn(chans ...<-chan int) <-chan int { // TODO: start one goroutine per input that forwards its values onto a // shared output channel, use a sync.WaitGroup to know when they've all // finished, then close the output. // // This stub returns an already-closed empty channel so the tests fail // (wrong values) instead of hanging. Replace it. ch := make(chan int) close(ch) return ch } func main() { a := make(chan int, 2) a <- 1 a <- 2 close(a) b := make(chan int, 1) b <- 3 close(b) for v := range fanIn(a, b) { fmt.Println(v) } }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.