Go Interview Prep #3: Channels
Channels are Go’s primary mechanism for passing data between goroutines. The mantra is: do not communicate by sharing memory; share memory by communicating.
Buffered vs unbuffered
An unbuffered channel has no internal queue. A send blocks until a receiver is ready, and vice versa. It provides synchronization as a side effect — both sides meet at the transfer (rendezvous).
ch := make(chan int) // unbuffered
A buffered channel has a queue of fixed capacity. A send only blocks when the buffer is full; a receive only blocks when the buffer is empty.
ch := make(chan int, 10) // buffered, capacity 10
Capacity zero is unbuffered: make(chan T, 0) is the same as make(chan T).
Directional channels
A channel can be restricted to send-only or receive-only at compile time. This documents intent and prevents misuse:
func producer(out chan<- int) { out <- 42 }
func consumer(in <-chan int) { fmt.Println(<-in) }
You can convert a bidirectional channel to a directional one when passing it, but not the other way around.
Closing a channel
Only the sender should close a channel. Closing signals “no more values will be sent”. Receivers can detect this with the comma-ok idiom or range:
close(ch)
v, ok := <-ch // ok = false when channel is closed and drained
for v := range ch { /* runs until ch is closed and drained */ }
Sending on a closed channel panics. Closing an already closed channel panics. Receiving from a closed, drained channel returns the zero value immediately (with ok = false).
Never close a channel from the receiver side if multiple senders exist — there is no safe built-in multi-sender close. Use a separate signal (context, WaitGroup + one closer) instead.
The select statement
select waits on multiple channel operations simultaneously, picking a ready one at random if several are ready at once:
select {
case msg := <-messages:
process(msg)
case <-time.After(time.Second):
return errors.New("timed out")
case <-ctx.Done():
return ctx.Err()
}
A default branch makes select non-blocking (try-send / try-receive). An empty select {} blocks forever — sometimes used intentionally to park the main goroutine.
Nil channels
A nil channel blocks on both send and receive forever. This is useful inside select to disable a case dynamically:
var ch chan int // nil
select {
case v := <-ch: // never selected while ch is nil
_ = v
case <-ctx.Done():
return
}
// After assigning a live channel, the case becomes eligible:
// ch = make(chan int, 1)
Closing or sending on a nil channel also panics (close) or blocks forever (send/receive).
Common patterns
Done / cancel channel — broadcast stop by closing:
done := make(chan struct{})
go func() {
select {
case <-done:
return
case <-work:
// ...
}
}()
close(done) // all receivers wake up
Prefer context.Context for cancellation in library and production code; a bare done channel is still a common interview pattern.
Fan-out — one input channel, multiple workers each reading from it (each value goes to exactly one worker).
Fan-in (merge) — multiple input channels, one or more goroutines that forward into a single output channel. Close the output only after all inputs are drained (often with a WaitGroup).
Pipeline — chain of stages where each reads from an input channel and writes to an output channel.
Or-done / timeout — wrap a receive with select on ctx.Done() or time.After so the caller never blocks forever.
Common interview questions
Q: What happens if you send on a closed channel?
The program panics. Design ownership so only the producer (or a single designated closer) closes the channel.
Q: Buffered or unbuffered — when to use which?
Unbuffered when you want synchronization between sender and receiver. Buffered when you want to decouple rates, limit in-flight work, or allow a send to complete after the receiver has gone (classic “buffer of 1” leak fix). A full buffer still blocks the sender.
Q: Can multiple goroutines receive from the same channel?
Yes. Each value is delivered to exactly one receiver. This is the basis for worker pools.
Q: Can multiple goroutines send on the same channel?
Yes. The channel serializes the sends safely. Closing is the hard part with multiple senders.
Q: What does ranging over a channel do?for v := range ch receives until ch is closed and its buffer is empty. If nobody closes ch, the loop never ends (and the ranging goroutine may leak).
Quick reference
| Operation | Unbuffered | Buffered (empty) | Buffered (partial) | Buffered (full) | Nil | Closed (drained) |
|---|---|---|---|---|---|---|
| Send | blocks | ok | ok | blocks | blocks forever | panic |
| Receive | blocks | blocks | ok | ok | blocks forever | zero + ok=false |
| Close | ok | ok | ok | ok | panic | panic |
