Week 9 - Channels, Deeply¶
9.1 Conceptual Core¶
- A channel is a typed, bounded (or unbounded), thread-safe queue with select integration. Internally it is a struct (
hchan) protected by a mutex, with two FIFO wait lists for blocked senders and receivers. - The CSP slogan ("share memory by communicating") is partly aspirational. In practice, large Go systems use channels for ownership transfer and signaling, and use mutexes/atomics for shared state. Both are idiomatic-picking the wrong one for a given problem is the bug.
- Send/receive semantics:
- Buffered channel with space → non-blocking send.
- Buffered channel full / unbuffered → block until a receiver is ready (or vice versa).
- Closed channel → send panics; receive returns zero value with
ok=false. nilchannel → send and receive block forever. Useful inselectto disable a case.
9.2 Mechanical Detail¶
Read src/runtime/chan.go. Particularly:
- hchan struct: qcount, dataqsiz, buf, elemsize, closed, sendx, recvx, recvq, sendq, lock.
- chansend: lock, then either copy to buffer / hand-off to waiting receiver / park sender.
- chanrecv: symmetric.
- closechan: marks closed, wakes all waiters.
- The hand-off optimization: if a sender finds a parked receiver, it copies directly into the receiver's stack and parks no goroutine. This is what makes unbuffered channels efficient.
- Select (runtime/select.go): randomized-fair selection across ready cases. The selectgo function is among the most subtle in the runtime; read it slowly. Note: select with a default is a non-blocking try.
- Closing discipline: close from the sender side, never from a receiver. Use sync.Once if multiple goroutines might close. The standard idiom for graceful shutdown is a separate done channel (or a context.Context), not closing the data channel.
9.3 Lab-"Channel Internals"¶
- Write a benchmark comparing: unbuffered chan, buffered chan(1), buffered chan(1024),
sync.Mutex+ slice queue, and a `sync/atomic - only SPSC ring buffer. Use 1 producer, 1 consumer, 10M messages. - Plot the throughput. The atomic SPSC should be 5–10× the channel; the mutex queue may beat the buffered channel for small messages.
- Reproduce a
nil - channel select pattern: a goroutine that toggles between two upstream channels by setting one tonil` to disable a case. - Write an "unbounded channel" using a goroutine that bridges an in-channel to an out-channel via an internal slice buffer. Discuss why this exists and why it is dangerous (memory growth on slow consumer).
9.4 Idiomatic & golangci-lint Drill¶
staticcheck SA1015(time.Tickleak),staticcheck SA1030(time.Afterin select-loops leaks),gocritic: emptyDecl,revive: empty-block. The first two are classic concurrency leaks.
9.5 Production Hardening Slice¶
- Add
goleak.VerifyTestMain(m)(Uber'sgo.uber.org/goleak) to the test entry point of every package that uses goroutines. CI will now fail any test that leaves a goroutine running.