Learn Go← Dashboard

Lightweight concurrent functions started with the go keyword.

Concurrency Patterns

The go keyword and channels are primitives. Real programs combine them into a handful of recurring shapes: worker pools, fan-out / fan-in, and pipelines. Recognise these and most Go concurrency code becomes legible.

Worker pool

You have N units of work and want at most K of them running at once. Spawn K goroutines that all read from the same jobs channel:

go playground
Loading...

Key rules:

  • Producers close jobs when no more work is coming. Workers exit naturally when range drains.
  • results is closed only after all senders finish — track them with a WaitGroup. Closing a channel that still has senders panics.
  • Output order is non-deterministic; if order matters, send {idx, value} records and sort at the end.

Fan-out, fan-in

Fan-out: many workers read from one upstream channel (the pattern above). Fan-in: many upstream channels merge into one downstream channel.

func merge(cs ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    wg.Add(len(cs))
    for _, c := range cs {
        go func(c <-chan int) {
            defer wg.Done()
            for v := range c { out <- v }
        }(c)
    }
    go func() { wg.Wait(); close(out) }()
    return out
}

Same closing rule: out closes only after every input has drained.

Pipeline (stages connected by channels)

Each stage is a goroutine reading from one channel and writing to the next:

gen := func(nums ...int) <-chan int {
    out := make(chan int)
    go func() { defer close(out); for _, n := range nums { out <- n } }()
    return out
}
sq := func(in <-chan int) <-chan int {
    out := make(chan int)
    go func() { defer close(out); for n := range in { out <- n * n } }()
    return out
}

for v := range sq(sq(gen(2, 3))) {
    fmt.Println(v) // 16, 81
}

Each stage owns its output channel and closes it on the way out. Composes cleanly — adding a filter stage is one more function.

When to skip channels entirely

Channels aren't a hammer. If you're guarding shared state, sync.Mutex is simpler and faster. If you're batching independent calls and want the first error to cancel the rest, errgroup gives you the pattern in three lines.

In a worker pool with 3 workers reading from `jobs` and writing to `results`, when can you safely `close(results)`?