How do nil channels behave in select, and how are they used to merge two channels?
Question 239HardGo 1.22 to 1.25
Sending to or receiving from a nil channel blocks forever. Inside a select, a case on a nil channel is therefore never chosen. Setting a channel variable to nil is the idiomatic way to disable a case dynamically.
func Merge2[T any](a, b <-chan T) <-chan T {
out := make(chan T)
go func() {
defer close(out)
for a != nil || b != nil {
select {
case v, ok := <-a:
if !ok {
a = nil // closed: stop selecting on it
continue
}
out <- v
case v, ok := <-b:
if !ok {
b = nil
continue
}
out <- v
}
}
}()
return out
}
Without the nil trick, a closed channel is always ready: it returns the zero value immediately. The loop would spin at 100% CPU, receiving zero values over and over.
Channel behavior summary:
- Nil channel: send blocks forever, receive blocks forever, close panics.
- Closed channel: send panics, receive returns the zero value with
ok == false, a second close panics.
The same trick is useful for "only send when I have something buffered": set outCh = nil while the pending queue is empty.
More on Concurrency Patterns & sync
- Q237Implement a generic worker pool with a fixed number of workers, context cancellation, and no goroutine leaks.
- Q238Implement fan-out / fan-in: a generic Merge that combines N channels into one.
- Q240Build a cancellable pipeline. What causes goroutine leaks in pipelines and how do you prevent them?
- Q241Spot the goroutine leak.
- Q242How do you close a channel safely when there are multiple senders?
- Q243How do you implement a semaphore in Go? Compare a buffered channel with golang.org/x/sync/semaphore.