What does a nil channel in Go do to a goroutine that sends or receives on it?
answer
- start from the zero value
- not empty, absent
- both directions park, neither returns
- one of the three operations crashes instead
basics
~10 sBoth operations block forever. A send on a nil channel and a receive from a nil channel park the goroutine permanently, because no counterparty can ever exist. Closing a nil channel panics instead.
solid answer
~50 sThe zero value of a channel type is nil, and every blocking operation on a nil channel blocks forever: `ch <- v` and `<-ch` both park the goroutine and it is never woken, because there is no channel object for any other goroutine to reach. It is not an error and not a panic — the operation is simply defined to block. The usual cause is a channel field or variable that was declared but never given to `make`, often because a constructor was bypassed with a struct literal. The one nil-channel operation that does not block is `close(ch)`, which panics. If every goroutine in the program ends up parked this way, the runtime aborts the process with `fatal error: all goroutines are asleep - deadlock!`. This blocking behaviour is not just a trap; it is what makes a nil channel usable to switch a `select` case off.
code
go · 9 linestype Mux struct {
frames chan Frame
}
func (m *Mux) NextFrame() Frame {
// if the constructor was skipped, m.frames is nil
// and this receive parks the goroutine forever
return <-m.frames
}go deeper
Recall the rule in both directions and the reason it comes up: the zero value of a channel is nil, so a declared-but-never-made channel behaves this way.
Explain why blocking rather than panicking is the coherent behaviour, that there is no wait queue to join, and that close is the exception that panics.
Be ready to recognise the symptom in production: a goroutine that stops progressing rather than failing, visible in a dump as chan receive (nil chan), and traced back to a skipped constructor.
Own the design consequence: constructors, not struct literals, should be the only way to build types containing channels, because a zero-value channel field fails silently instead of loudly.
## The zero value of a channel A channel is a reference type. `var ch chan int` declares a channel variable and initialises it to its zero value, `nil`. Only `make(chan int)` or `make(chan int, n)` allocates the runtime channel that a nil channel variable does not point to. The language then defines, plainly: - A **send** on a nil channel blocks forever. - A **receive** from a nil channel blocks forever. - `close` on a nil channel **panics**. No error, no panic and no zero value comes back from the first two. The goroutine parks and is never rescheduled. ## Why blocking rather than panicking The reason is mechanical rather than philosophical. A goroutine blocked on a channel is placed on that channel's wait queue; it is woken when another goroutine performs the matching operation on **the same channel**. With a nil channel there is no channel object and therefore no queue to be placed on and no handle any other goroutine could name in order to wake it. Blocking forever is the honest description of a wait that nothing can satisfy. The contrast with a legitimate blocked receive matters. `<-make(chan int)` also blocks right now, but it is blocked on a real object that another goroutine can send to or close. A nil-channel wait has no such escape. ## How you meet it by accident The overwhelmingly common cause is a channel that was declared but never made: - A struct field of channel type populated by a struct literal that skipped the constructor. - A package-level `var done chan struct{}` where the `make` lives in an `init` path that did not run. - A channel returned from a function that took an early error return and returned the zero value. The symptom is not a crash at the point of the mistake. It is a goroutine that silently stops making progress at the first send or receive. In a program that is otherwise idle, the runtime notices that no goroutine can ever run again and aborts with `fatal error: all goroutines are asleep - deadlock!`. In a busy server other goroutines are still runnable, so the process keeps serving and the stuck goroutine just accumulates, which is why this often surfaces as a slow leak rather than a crash. ## Reading it in a goroutine dump When the runtime dumps goroutines, each status line names what the goroutine is parked on. A nil-channel wait is spelled out explicitly: ```text goroutine 6 [chan receive (nil chan)]: goroutine 7 [chan send (nil chan)]: ``` That parenthesised `(nil chan)` is the direct clue: it is not a slow producer or a full buffer, the channel value itself is nil. Seeing it, you look for the missing `make`, not for a missing sender. ## The useful half Because a nil channel is never ready, a `select` case whose channel expression evaluates to nil can never be chosen. Assigning nil to the variable a case reads therefore switches that case off for as long as the variable stays nil, and assigning a real channel back switches it on again. That is the standard way to retire a source that has finished without restructuring the loop — a deliberate use of exactly the behaviour that bites people by accident. The caution that goes with it: if *every* case in a `select` is nil and there is no `default`, the `select` can never proceed, and the goroutine is parked as surely as on a bare nil receive. ## Close is the exception `close(ch)` on a nil channel panics with `close of nil channel`. It is worth remembering as a pair with the blocking rules, because it breaks the pattern: two of the three nil-channel operations block and the third crashes. Guarding with `if ch != nil { close(ch) }` is legal but usually a sign that ownership of the channel is unclear. ## What a good answer includes State the rule for both directions, name the zero value as the reason a nil channel shows up at all, note the panic on close, and mention that the blocking behaviour is deliberately exploited to disable a `select` case rather than being purely a hazard.
- What does close on a nil channel do?It panics with `close of nil channel`. That is the one nil-channel operation that does not block. If you find yourself writing `if ch != nil { close(ch) }`, it usually means ownership of the channel is unclear rather than that the guard is genuinely needed.
- How does blocking on a nil channel differ from blocking on a made channel with no sender?Mechanically the goroutine is parked either way, but a made channel has a real wait queue: another goroutine can send to it or close it and wake the waiter. A nil channel has no object, so nothing can ever name it to wake the goroutine. A dump distinguishes them, showing `(nil chan)` in the wait reason.
- Is a nil channel ever useful rather than a bug?Yes. Because a nil channel is never ready, a `select` case reading a nil channel variable can never fire. Assigning nil to that variable disables the case and assigning a real channel back re-enables it, which is the normal way to retire a finished source inside a long-lived select loop.
An empty channel is a pipe with nothing in it yet. A nil channel is no pipe at all, so waiting at the end of it is waiting at a wall.
saying these in an interview costs you the question
- Says a receive from a nil channel returns the zero value immediately
- Says operating on a nil channel panics with a nil dereference
- Confuses a nil channel with a closed channel
- Assumes declaring var ch chan int allocates the channel
- Thinks close on a nil channel is a harmless no-op