Saltar a contenido

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.
  • nil channel → send and receive block forever. Useful in select to 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"

  1. 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.
  2. Plot the throughput. The atomic SPSC should be 5–10× the channel; the mutex queue may beat the buffered channel for small messages.
  3. Reproduce a nil - channel select pattern: a goroutine that toggles between two upstream channels by setting one tonil` to disable a case.
  4. 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.Tick leak), staticcheck SA1030 (time.After in 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's go.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.

Comments