a common mistake when using Closing Channels
Fast recall
Quick revision
Core answer
Practical understanding of Closing Channels.
Repeating a definition without saying when to use it.
Give one trade-off and say how you would test it.
Interview-ready guide
Interview answer
close on an already-closed channel panics, immediately:
ch := make(chan int)
close(ch)
close(ch) // panic: close of closed channelThe panic text is verbatim close of closed channel. Same loud-failure philosophy as send-after-close: a double close means two owners each believed they held the lifecycle — an ownership bug that would otherwise silently confuse receivers (the second close's intent is unknowable: 'even MORE done'?). The panic is the stack trace pointing at the second closer.
The common real-world trigger: two goroutines both 'cleaning up' the same channel — a shutdown path that closes, then a defer that closes again (double-defer, or a defer plus an explicit close in an error branch). The fix is the single-owner discipline made mechanical: exactly ONE defer close(ch) in exactly one function — the producer — and no other close anywhere. When multiple senders exist, the fan-in or WaitGroup patterns centralize the close into one place precisely so this panic class cannot happen.
The state model makes all three close-related panics/blocks one family: closed channel + close → panic (double close); closed channel + send → panic (send after close); closed channel + receive → graceful drain then zero/false (the only gentle one, because receiving is the passive side — it can only observe). Every panic in the channel API is a lifecycle-contract violation; the design chose to make them loud rather than let lifecycle bugs become silent data-flow bugs.
a common mistake when using Closing Channels
Direct Answer
No — the second close panics: close of closed channel. Close is one-way and one-shot by design.
Deep Dive & Architecture
Why panic on the second close, mechanically: close(ch) is not idempotent because the closed flag is a state transition, not a set operation. The first close sets the flag and wakes the waiters. The second close finds the flag set — and now the operation is being invoked on a channel whose state it already produced. Two interpretations exist: 'redundant no-op' or 'contract violation'. Go chose violation because a second close is ALWAYS a second owner believing they held the lifecycle — the exact ownership confusion the close discipline exists to prevent. Silent no-op would let that bug live forever; panic puts a stack trace at the second closer's feet.
The three channel panics as one family:
- close of closed channel — two owners.
- send on closed channel — a sender that missed the shutdown.
- (the contrast case) receive on closed channel — graceful, because receiving is observing, and observation can't corrupt.
The design principle, stated once: channel operations that mutate the channel's lifecycle contract panic on violation; operations that merely observe degrade gracefully. That's why the panic cases cluster on the producer side — lifecycle is producer territory, and producer-side bugs are the ones that corrupt consumers.
The fix pattern for double-close-shaped code: never write close in a branch. The lifecycle close lives in exactly one place — defer close(ch) at the top of the single owner function — so the path count through the close is exactly one. Multiple close calls in different branches is the same bug as multiple owners, just syntactically hidden.
Common Mistakes & Pitfalls
Repeating a definition without saying when to use it.
Check your understanding
Answer these without looking back at the article.
- What is Closing Channels, and when would you use it?
- When would you use Closing Channels in a real project?
- How would you compare Closing Channels with an alternative?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.