Go

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

All 38 Concurrency Patterns & sync questions