Lesson 15 · Concurrency & context

Channels & select

"Share memory by communicating" — how goroutines actually cooperate.

Your win: send and receive on channels, tell buffered from unbuffered, and use select — especially the ctx.Done() case that makes a goroutine cancellable.

A channel is a typed conduit

ch := make(chan int)       // unbuffered
ch := make(chan int, 8)    // buffered, capacity 8
ch <- v                     // send
v := <-ch                   // receive
v, ok := <-ch               // ok == false if channel closed & drained
close(ch)                   // sender closes; never the receiver
for v := range ch { ... }   // receive until closed
Buffered vs unbuffered — the key difference An unbuffered send blocks until a receiver is ready — so it doubles as a synchronisation point (a handshake). A buffered send only blocks when the buffer is full. Pick unbuffered when you want the two goroutines to rendezvous; buffered when you want to decouple their speeds.

Go's motto: "Don't communicate by sharing memory; share memory by communicating." Instead of guarding a shared variable with a lock, pass the value down a channel — ownership moves with it, and the send/receive is itself synchronised.1

select: wait on several channels at once

select blocks until one of its cases can proceed; if several are ready it picks one at random; a default makes it non-blocking. The most important case in production Go is ctx.Done() — it's how a goroutine honours cancellation:

select {
case msg := <-work:
    process(msg)
case <-ctx.Done():     // cancelled/timed out → stop cleanly
    return ctx.Err()
}

Our consumer loops use exactly this to back off and stay cancellable — selecting between a retry timer and the context's Done() (golibs/kafka/kafka.go:672-771). That's how Close() can stop a wedged consumer instead of leaking it (Lesson 14).

Your fan-in, decoded

The clearest channel example in our services collects results from two parallel repo calls:

conversationmgmt/.../support_conversation_reader_staff_get_conversations.go:48-93
errChan := make(chan error, 2)          // buffered = #producers → sends never block
go func() { errChan <- repoA(...) }()
go func() { errChan <- repoB(...) }()
go func() { wg.Wait(); close(errChan) }() // closer: signal "all done"
for err := range errChan { multierr.Append(...) } // aggregate
Why this is race-free No shared variable, no lock. Each result travels down the channel (the send happens-before the receive — Lesson 19), the buffer is sized to the producer count so sends never block, and closing after Wait() cleanly ends the range. "Share memory by communicating," in your codebase.
Channel rules that bite Sending on a closed channel panics. Sending on a nil channel blocks forever. Only the sender should close — and only once. These show up constantly in interviews.
Read / watch this next

Rob Pike — Go Concurrency Patterns (slides)

Channels as a composition tool — generators, fan-in, pipelines. Then the Tour's channels & select pages for hands-on.

go.dev/talks/2012/concurrency.slide
A Tour of Go — channels & select

Check yourself (from memory)

Q1. A send on an unbuffered channel…

Unbuffered = rendezvous: the send waits for a receive, making it a synchronisation point. Buffered blocks only when full.

Q2. select is used to…

It proceeds with whichever case is ready (random if several); default makes it non-blocking.

Q3. Adding case <-ctx.Done(): to a select lets a goroutine…

It's how a goroutine honours cancellation/timeout and avoids leaking — the backbone of our consumer loops.
How does our fan-in collect results from N goroutines with no lock and no race?
recall, then click to reveal
Each goroutine sends its result down a buffered error channel sized to the number of producers (so sends never block). A closer goroutine runs wg.Wait(); close(errChan), then the main goroutine ranges the channel and aggregates with multierr. The channel transfers each value safely (send happens-before receive) — no shared variable, no mutex. "Share memory by communicating."
Want the classic patterns — generator, fan-out/fan-in, pipeline — built up from these primitives? That's Lesson 18; ask me for a preview.

1. Go Concurrency Patterns; Effective Go — channels; Go Proverbs.