panic: send on closed channel
Go Programming Language
Severity: CriticalWhat Does This Error Mean?
This panic means a goroutine tried to send data to a channel that has already been closed. In Go, once a channel is closed, you can no longer send to it — only receive remaining values. Sending to a closed channel causes an immediate panic. The fix is to ensure only one goroutine is responsible for closing a channel and only after all sends are complete.
Affected Models
- Go 1.0 and later
- All Go versions
Common Causes
- Closing a channel and then trying to send more values to it
- Multiple goroutines closing the same channel — the second close also panics
- A goroutine sends to a channel after another goroutine has already closed it (race condition)
- Closing a channel in a defer while the function might still be sending to it
- Not synchronizing channel close operations between goroutines
How to Fix It
-
Follow the rule: only the goroutine that sends to a channel should close it — and only after it is done sending.
Never close a channel from the receiver side. The receiver has no way to know if the sender is done. The sender always knows when it has sent everything.
-
When multiple goroutines send to a single channel, use sync.WaitGroup to wait for all senders to finish, then close the channel in a coordinator goroutine.
go func() { wg.Wait(); close(ch) }() — this closes the channel only after all senders have called wg.Done()
-
Never close the same channel twice. Use a sync.Once to guarantee a channel is only closed one time.
var once sync.Once; closeChannel := func() { once.Do(func() { close(ch) }) } — calling closeChannel() multiple times is now safe.
-
Use the context package for cancellation signals instead of closing channels for signaling. context.CancelFunc is designed for this pattern.
Create a cancelable context: ctx, cancel := context.WithCancel(context.Background()) — goroutines check ctx.Done() and cancel() can be called multiple times safely.
-
Add logging or use the Go race detector (go run -race yourfile.go) to find race conditions where channels are closed before all sends complete.
The race detector is a built-in Go tool that detects concurrent access issues at runtime. Run tests with -race flag: go test -race ./...
When to Call a Professional
Channel panics in Go are serious concurrency bugs. The general rule: only the sender should close a channel, and only after all sends are complete. Use sync.WaitGroup and careful design to coordinate channel lifetimes.
Frequently Asked Questions
What is a channel in Go?
A channel is a typed pipe that goroutines use to communicate with each other. One goroutine sends values into the channel; another goroutine receives them. Channels make concurrent code safe because they transfer data between goroutines without shared memory. Create a channel: ch := make(chan int). Send: ch <- 42. Receive: value := <-ch.
Why does reading from a closed channel not panic but writing does?
By design — closed channels can still have buffered values that need to be received. Reading from a closed channel returns remaining buffered values, then returns zero values with ok=false when empty. This is how Go signals 'no more data' to receivers. Writing to a closed channel makes no sense — the channel is done, so Go panics to catch this programming error.
How do I check if a channel is closed?
Use the two-value receive form: value, ok := <-ch — if ok is false, the channel is closed and no more values will come. There is no way to check if a channel is closed without receiving from it. This is intentional — the receiver finds out the channel is done when it drains the last value. The for range loop over a channel automatically handles this: for v := range ch { ... } stops when ch is closed.