Channels
Go builds concurrency around one slogan — "don't share memory, communicate over channels". A channel is not just a queue: it is a typed conduit that synchronizes goroutines and establishes a happens-before relationship between them. Pass a value through a channel and the compiler plus the runtime guarantee the receiver sees everything the sender wrote before the send. That is exactly why channels replace mutexes in idiomatic Go code.
But behind that convenience hides a set of sharp edges, and each is a standard interview question. A send on a closed channel is not "silently dropped" — it crashes the program with a panic. An operation on a nil channel does not panic — it blocks forever. Closing from a receiver or from multiple senders panics on a double close. A select with no default blocks, an empty select{} blocks forever, and a goroutine blocked forever on a channel nobody will satisfy is a leak the garbage collector will not reclaim. This topic dissects the channel layer by layer — from the runtime hchan structure to the tree of context nodes.
Topic Map
- Channel mechanics — what a channel is under the hood — the
hchanstructure, the unbuffered rendezvous, the happens-before relationship. - Inside the hchan struct — the ring buffer with
sendx/recvx, thesendq/recvqqueues of parked goroutines, and the send decision path. - Buffered channels —
make(chan T, n), the ring buffer, exactly when a send and a receive block. - Nil channels — why a send and a receive on a
nilchannel hang forever, and how this is used inselect. - Closed channel behavior — comma-ok,
rangeover a channel, the panic on send and on a repeated close. - The select statement — the uniformly-random choice of a ready case,
defaultas the non-blocking path, the emptyselect{}. - Timeout with select —
selectwith a timer case, thetime.Afterleak in a loop, and thetime.NewTimer+Stopfix. - Goroutine leaks — how a goroutine blocked forever quietly accumulates and why the GC will not reclaim it.
- Context cancellation — how
context.Contextcarries the cancellation signal and propagates it down the tree. - Context internals —
cancelCtx,timerCtx,valueCtx, the lazyDone, the value chain.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
| Believing a buffered channel never blocks the sender | A send blocks once the buffer is full; a false throughput model |
| Thinking an unbuffered channel has a buffer of size one | Its size is zero — a synchronous rendezvous, not a "buffer for one" |
Believing a nil channel operation panics | It blocks forever; confused with a closed channel, it is a silent deadlock |
| Thinking a send on a closed channel is silently dropped | A send on closed channel panic — the program crashes |
| Closing a channel from a receiver or from multiple senders | A double close and a send on a closed channel both panic |
Inventing a closed(ch) built-in | Go has no such predicate; closure is detected via comma-ok |
Believing select cases have priority by source order | The choice among ready cases is uniformly random |
Thinking default makes select poll the channels in a loop | default runs at once if no case is ready — one pass, not a busy-poll |
| Assuming the GC reclaims a goroutine blocked on a channel forever | A blocked goroutine is reachable from the runtime — that is a leak, not garbage |
Thinking cancelling a context forcibly kills goroutines | Cancellation only closes Done(); the goroutine must check the signal itself |
| Believing cancellation flows from child to parent | Cancellation propagates from the parent to all child contexts |
Forgetting to call the cancel func | A timerCtx timer leaks and the node stays in the tree until the parent is cancelled |
Interview Relevance
Channels are a mandatory topic on any Go interview, and the question is not the <- syntax but the blocking model beneath it. The interviewer checks whether you have a working mental model of when an operation blocks, when it panics, and when it hangs forever.
What interviewers check:
- How a buffered channel differs from an unbuffered one — where the synchronous rendezvous is, and where the ring buffer.
- Exactly when a send and a receive block — on an empty and on a full buffer.
- What happens on an operation on a
nilchannel — blocking forever, and how this is used inselect. - What a send on a closed channel does — a panic; what a receive returns — the zero value and
ok=false. - The semantics of
select— the uniformly-random choice, the role ofdefault, the behavior of an emptyselect{}. - How a channel leads to a goroutine leak and how it is prevented — a
selectonctx.Done()or a buffered result channel. - How
context.Contextpropagates cancellation and how it is built internally — the tree ofcancelCtx/timerCtx/valueCtx.
A typical wrong answer: "a send on a closed channel is just silently dropped, it is safe". That triggers a discussion of how a send on a closed channel panics immediately, how closing is the responsibility of exactly one owner-sender, and how a receiver only detects closure.