Goroutine + Channel Fan-Out
Fan-out is the canonical Go concurrency pattern: a producer pushes jobs onto a channel, a fixed pool of worker goroutines pulls and processes them in parallel, and results flow back through a results channel. This snippet shows a minimal worker pool, the close + range idiom that signals completion, and a `sync.WaitGroup` variant for fire-and-forget. Use this whenever you need parallelism with a bounded number of workers.
857 views
15
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
time.Sleep(20 * time.Millisecond) // pretend work
results <- j * j
_ = id
}
}
func main() {
const numWorkers = 3
jobs := make(chan int, 10)
results := make(chan int, 10)
var wg sync.WaitGroup
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
worker(id, jobs, results)
}(w)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs) // signals workers to exit their range loops.
go func() { wg.Wait(); close(results) }()
sum := 0
for r := range results {
sum += r
}
fmt.Println("sum of squares =", sum)
}Two channels carry direction: jobs flows producer to workers, results flows workers to consumer. The worker uses for j := range jobs which exits cleanly when jobs is closed; that is what the close(jobs) after the producer loop does. A sync.WaitGroup tracks how many worker goroutines are still running so the consumer can close results only when all workers have finished. The buffered channels (make(chan int, 10)) decouple producer and consumer enough to overlap, but pick the buffer size based on actual workload, not by reflex. The whole pattern is O(jobs / workers) wall time at the cost of one goroutine per worker plus two channels.
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 1; i <= 3; i++ {
ch <- i
}
close(ch)
}()
// ranging over a channel exits when it is closed AND drained.
for v := range ch {
fmt.Println("recv", v)
}
fmt.Println("channel closed; loop exited cleanly")
}Closing a channel signals 'no more values will be sent'. Receivers ranging over the channel keep reading until the buffer is drained, then exit the loop. Sending on a closed channel panics, so the rule is: only the SENDER closes, never the receiver. When multiple senders exist, coordinate them with a sync.WaitGroup and have a single goroutine close the channel after wg.Wait() returns (the pattern in accordion 1). Closing an unbuffered channel also unblocks any receiver that was waiting, returning the zero value plus ok = false from the comma-ok form.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
items := []string{"alpha", "beta", "gamma"}
for _, it := range items {
wg.Add(1)
item := it // capture by value, not by reference, for older Go versions
go func() {
defer wg.Done()
time.Sleep(10 * time.Millisecond)
fmt.Println("processed", item)
}()
}
wg.Wait()
fmt.Println("all goroutines done")
}When you do not need return values, a sync.WaitGroup plus a goroutine per item is the simplest possible parallel-foreach. wg.Add(1) BEFORE spawning, defer wg.Done() inside, wg.Wait() at the end. The local copy item := it is essential on Go 1.21 and earlier because the loop variable was reused across iterations; without the copy, every goroutine would see whatever the loop variable held at the time it started. Go 1.22 fixed the loop-variable semantics, but the explicit copy is still a safe habit. Avoid spawning unbounded goroutines this way over user input; for that, use the worker-pool pattern in accordion 1.
