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

OperationUnbufferedBuffered (empty)Buffered (partial)Buffered (full)NilClosed (drained)
Sendblocksokokblocksblocks foreverpanic
Receiveblocksblocksokokblocks foreverzero + ok=false
Closeokokokokpanicpanic