Several parser goroutines each close the shared records channel and it panics with "send on closed channel". How do you fix it?
answer
- close describes the stream, not your share of it
- no sender can know it is the last
- sync.Once removes the wrong panic
- let the goroutine that launched them close
- or never close the data channel, only done
basics
~20 sclose describes the whole channel, not one sender's share of it, so with several senders none may close. Let the single owner that started them close once they have all returned, or leave the data channel open and close a separate done channel instead.
solid answer
~50 sEach parser is closing a channel that other parsers are still sending on, so whichever finishes first ends the stream for everyone: the next sender panics with `send on closed channel`, and a slower closer panics with `close of closed channel`. The fix is not `sync.Once` — that only removes the double-close panic and leaves the send-after-close one, which is the harder of the two to reproduce. `close` is a statement about the channel as a whole, and no individual sender can know it is the last, so **no sender closes**. Two shapes work. If the consumer must see the end of the stream, the goroutine that launched the parsers joins them all and closes `records` afterwards, when by construction no sender remains. If instead the *consumer* wants to stop early, keep `records` open and close a separate `done chan struct{}`: closing broadcasts to every parser, each of which selects between sending and giving up.
code
go · 11 linestype Record struct{ File string }
func parse(files []string, records chan<- Record, done <-chan struct{}) {
for _, f := range files {
select {
case records <- Record{File: f}:
case <-done: // closed by the consumer; every parser sees it
return
}
}
}go deeper
Remember the one rule this failure comes from: the sending side closes, and only when it is the only sender. A receiver never closes a channel it reads from.
Explain why close cannot mean "this sender is done": it is one fact about the whole stream, and no sender knows whether its peers have finished.
Diagnose it from the panic — a producer function run by several goroutines containing its own close — reject sync.Once with a reason, and pick between owner-closes and never-close by asking who is ending the work.
Make channel ownership a stated rule rather than a review catch: direction-typed parameters at every boundary, one close site per channel, and a documented answer to who cancels whom.
## Reading the panic `panic: send on closed channel` with a stack whose top frame is inside a parser goroutine tells you two things. The parser was still legitimately producing, and somebody had already declared the stream finished. The closing goroutine is usually *not* on the trace, because its `close` call returned long before the panic — so the trace names the victim, not the culprit. When several goroutines run the same producer function, that is the strongest hint: look for a `close` inside the function that more than one goroutine runs. In a code generator whose parsers each walk part of a schema and emit record descriptors onto one shared channel, the buggy shape is exactly this: ```go func parse(files []string, records chan<- Record) { defer close(records) // wrong: every parser does this for _, f := range files { records <- Record{File: f} } } ``` The `defer close(records)` reads like "I'm done", but that is not what `close` means. ## Why `close` cannot be per-sender `close` records one fact about the channel: no further values will ever be sent on it, by anyone. It is a statement about the *stream*, not about the calling goroutine's contribution to it. With N senders, no individual sender has the information needed to make that statement — it does not know whether its peers have finished. This is the point that engineers coming from languages whose queues expose a closeable, multi-writer API most often have to unlearn: there, `close` frequently means "this writer is finished", and the queue tracks the writer count for you. Go's channel keeps no such count. ## Why `sync.Once` is not the fix The instinct after seeing `close of closed channel` is to make the close happen once: ```go var once sync.Once once.Do(func() { close(records) }) ``` That is a correct fix for the wrong problem. The double close is gone, but the *first* close still happens while other parsers are mid-loop, so the failure mode shifts from a panic in the closer to `send on closed channel` in a peer — non-deterministic, dependent on scheduling and input size, and far more likely to reach production before it is noticed. `sync.Once` around a close is only appropriate when you already know no sender can still be running; and if you know that, you did not need the `Once`. ## Fix 1: one owner closes, after every sender has returned The goroutine that started the parsers is the only party that knows when they have all finished. So it joins them and closes afterwards: - launch the parsers, - wait for every one of them to return, - then `close(records)`, - and the consumer's `for rec := range records` loop ends by itself. By construction there is no sender alive when `close` runs, so neither panic is reachable. Note the structural requirement this creates: the closing goroutine must not be the one draining `records`, or it will block waiting for parsers that are themselves blocked sending to it. The consumer runs concurrently with the join. ## Fix 2: don't close the data channel at all If the reason the parsers must stop is that the *consumer* is abandoning the job — the generator hit a fatal template error and no further records are wanted — then closing `records` is doubly wrong: the receiver would be closing a channel it only reads. Instead, keep `records` open and have the receiver close a separate `done chan struct{}`. Closing broadcasts, so every parser sees it, and each one selects between making progress and giving up: A parser's send becomes a `select` between `records <- rec` and `<-done`. Nobody ever closes `records`, which is fine: closing is only needed when a receiver's control flow depends on the end of the stream, and here the receiver is the one calling it off. ## Encoding the rule in signatures Write directions into parameter types. A parser that takes `records chan<- Record` and `done <-chan struct{}` can send records and can observe cancellation, and the compiler rejects `close(done)` inside it because a receive-only channel cannot be closed. The convention "the sender closes, and only when it is the sole sender" then stops being a comment and becomes something review does not have to catch by eye. ## What to say in the interview Diagnose from the panic (a producer function run by several goroutines that contains its own `close`), reject `sync.Once` explicitly and say why (it moves the panic rather than removing it), then give both fixes and the condition that selects between them: close when the consumer must detect the end, and don't close at all when the consumer is the one calling it off.
- Why can the consumer not simply close the records channel to make the parsers stop?Because closing does not stop senders — it panics them. The next `records <- rec` fails with `send on closed channel`, so a receiver-side close converts a clean shutdown into a crash. Cancellation flows the other way: the consumer closes a `done` channel the parsers watch. Typing the parameter as `<-chan Record` on the consumer side makes the compiler enforce this.
- Where exactly does close(records) go once you adopt an owner-closes rule?In the goroutine that launched the parsers, immediately after it has joined all of them — it is the only party that knows no sender remains. Crucially it must not also be the goroutine draining `records`, or it will wait for parsers that are themselves blocked trying to send. The consumer runs concurrently and its range loop ends when the close lands.
- Is there a case where wrapping close in sync.Once is the right answer?Only when you already know every sender has finished and you are merely guarding against two shutdown paths racing to close — a `Close` method that may be called twice, say. It protects against a double close, never against a live sender. If you are reaching for it because senders might still be running, it is hiding the bug rather than fixing it.
- How do you reproduce this reliably enough to trust the fix?Widen the window: run the parsers over enough input that they overlap, run the test with `-race` and `-count` so scheduling varies across runs, and assert that the consumer received every record rather than only that the program did not crash. A close-related bug that only shows up under load is usually a timing-narrow version of exactly this one.
One parser closing the shared channel is like one of several authors mailing the "manuscript complete" note to the printer while their co-authors are still posting chapters. The note is about the book, not about one writer's share of it.
saying these in an interview costs you the question
- Has each sender close the shared channel when it finishes
- Wraps the close in sync.Once and calls the design fixed
- Recovers from the send-on-closed panic instead of fixing ownership
- Lets the consumer close the data channel to stop the producers
- Assumes close waits for in-flight sends to complete
- Blames the goroutine on top of the panic stack for the bug