skip to content

Which Go channel operations panic once the channel is closed or nil?

level: middleimportance: must knowfreq 66%

answer

  1. three of them, and one safe direction
  2. receiving is never the problem
  3. close is not idempotent
  4. the runtime has nowhere to put the value
  5. a nil channel blocks, but closing one does not

basics

~10 s

Three operations panic: sending on a closed Go channel, closing an already-closed channel, and closing a nil channel. Receiving from a closed channel never panics; it returns the zero value immediately with ok false.

solid answer

~50 s

There are exactly three panics. `ch <- v` on a closed channel panics with `send on closed channel`. `close(ch)` on an already-closed channel panics with `close of closed channel`. `close(ch)` where `ch` is nil panics with `close of nil channel`. Receiving is the safe direction: a receive from a closed channel always succeeds immediately, returning the zero value with `ok == false`, no matter how many times you do it. The asymmetry is deliberate. A send after close has nowhere to go — the runtime cannot queue it, drop it silently, or return an error, because `ch <- v` is a statement with no result — so it is treated as the program bug it is. All three are ordinary panics that a deferred `recover` in the same goroutine can catch, but recovering is not a fix: the panic means two goroutines disagree about who owns the channel's lifetime, and the value being sent is lost either way.

code

go · 8 lines
go
ch := make(chan int, 1)
close(ch)

v, ok := <-ch // safe: 0, false — and it stays that way
fmt.Println(v, ok)

ch <- 1   // panic: send on closed channel
close(ch) // panic: close of closed channel (if you ever got here)

go deeper

for a junior

Memorise the three panicking operations and the one direction that is always safe. Being able to recite them, with the exact runtime messages, is what is being checked here.

for a middle

Explain why the runtime panics rather than returning an error: a send is a statement with no result, and a post-close send means two goroutines disagree about who ends the stream.

for a senior

Demonstrate that you read such a panic as an ownership bug. Say how you would locate the premature close from a trace whose top frame is the innocent sender.

for a principal

Own the rule rather than the incident: one close site per channel, direction-typed parameters at package boundaries, and recover around a send treated in review as an unresolved design question.

## The complete inventory For a channel `ch` of element type `T`: | Operation | Channel open | Channel closed | Channel nil | |---|---|---|---| | `ch <- v` (send) | blocks until received or buffered | **panics**: `send on closed channel` | blocks forever | | `<-ch` (receive) | blocks until a value arrives | returns zero value, `ok == false` | blocks forever | | `close(ch)` | marks it closed | **panics**: `close of closed channel` | **panics**: `close of nil channel` | That is the whole surface. Three panics, all on the send/close side; the receive side never panics. ## Why a send after close is a panic and not an error Go could have made a post-close send a no-op, but a send is a statement, not a function call — there is no return value to carry a failure, and silently dropping the value would turn a lost message into an invisible bug. More fundamentally, the situation cannot be recovered from in any useful sense: the sender believed it still owned the right to produce values, while somebody had already declared the stream finished. That is a disagreement about ownership, and the runtime surfaces it loudly and immediately at the exact line that is wrong. ## Why closing twice panics `close` is not idempotent, which surprises people arriving from APIs where calling `Close` twice on a handle is harmless or returns an error. Again the reasoning is that a second close means two pieces of code both believe they own the end of the stream. Wrapping the close in `sync.Once` will make the double-close panic go away, but it does not make anything correct if senders may still be running — it converts a `close of closed channel` panic into a `send on closed channel` panic, which is the harder one to reproduce. ## Why closing a nil channel panics while sending on one does not This asymmetry looks arbitrary but is not. Blocking forever on a nil channel is a *useful* state: it is how a `select` case is deliberately disabled, so the language leaves send and receive on nil well-defined and silent. Closing a nil channel, by contrast, can never mean anything — there is no channel whose state could be marked finished — so it can only be a bug, most often a channel field that was never initialised with `make`. The panic points straight at the missing initialisation. `var ch chan int` gives you a nil channel; so does the zero value of a struct field of channel type. Forgetting `make` in a constructor and then closing in a shutdown path is the usual way to meet `close of nil channel`. ## Recoverable, but not a fix All three are regular panics. A `defer func() { recover() }()` in the panicking goroutine will catch them, and code sometimes does this to keep a server alive. It is worth being clear about what that buys you: the value being sent is gone, the ownership bug is still there, and the next occurrence may land somewhere less convenient. Note the contrast with the runtime's `all goroutines are asleep - deadlock!` report, which is a fatal error rather than a panic and cannot be recovered at all. The honest fix is structural: exactly one place in the code closes any given channel, and that place runs only when no sender can still be running. ## Reading the panic The panic message names the operation, and the stack trace's top frame is the goroutine that did it. For `send on closed channel` that frame is the *sender*, which is useful but slightly misleading — the sender is the victim. The bug is wherever `close` was called, and that call is usually not on the stack at all, because it already returned. So the trace tells you which producer was still alive; you then have to find the code that decided the stream was finished. ## Practical rules - Close in exactly one place per channel, on the sending side. - Prefer `defer close(out)` at the top of the single producer goroutine, so the close is visibly tied to that goroutine's lifetime. - Never close a channel you only receive from; declare such parameters as `<-chan T` and the compiler will enforce it. - Treat a `recover` around a channel send as a marker for an unresolved ownership question, not as a solution.

  • Is the send on closed channel panic recoverable?
    Yes — it is an ordinary panic, so a deferred `recover` in that same goroutine catches it. But the value being sent is lost and the ownership bug survives, so recovering only hides it. Contrast the runtime's `all goroutines are asleep - deadlock!` report, which is a fatal error, not a panic, and cannot be recovered at all.
  • Why does close of a nil channel panic when a send on a nil channel merely blocks?
    Blocking on a nil channel is deliberately useful: setting a channel variable to nil is how you disable a `select` case, so send and receive on nil are well-defined and silent. Closing a nil channel can never be meaningful — there is no channel state to mark finished — so it can only be a missing `make`, and the runtime says so.
  • The stack trace's top frame for a send on closed channel panic is the sender. Is that where the bug is?
    Usually not. The sender is the goroutine that was still legitimately producing; the bug is wherever `close` was called too early, and that call has typically already returned so it is not on the trace at all. Use the trace to identify which producer was still alive, then go find the code that decided the stream was over.

saying these in an interview costs you the question

  • Says sending on a closed channel returns an error
  • Believes close is idempotent, like calling Close twice on a file
  • Thinks close of a nil channel is a harmless no-op
  • Wraps the close in recover and calls the design safe
  • Claims receiving from a closed channel panics