Channels

intermediate go concurrency

A channel is a typed pipe that goroutines use to send and receive values. One goroutine puts something in, another takes it out.

There’s a famous Go saying that sums up the whole philosophy:

“Don’t communicate by sharing memory; share memory by communicating.”

In simple language: instead of multiple goroutines poking at the same variable behind locks, we pass data through a channel. Whoever holds the value owns it. No locks, no races.

Making and using a channel

We create a channel with make, send with ch <- x, and receive with <-ch.

ch := make(chan int) // a channel that carries ints

go func() {
	ch <- 42 // send 42 into the channel
}()

value := <-ch // receive from the channel, blocks until something arrives
fmt.Println(value) // 42

The arrow always points in the direction the data flows. ch <- x means “x goes into ch”. <-ch means “pull a value out of ch”.

Unbuffered vs buffered

This is the part interviewers dig into.

An unbuffered channel (make(chan int)) has no storage. A send blocks until someone is ready to receive, and a receive blocks until someone is ready to send. It’s a synchronous handshake — both sides meet at the same moment.

A buffered channel (make(chan int, 3)) has a little waiting room. Sends only block when the buffer is full; receives only block when it’s empty. So the sender can drop a few values and walk away without waiting.

Unbuffered — handshake
sender → ⟷ → receiver
both block until they meet
Buffered (cap 3)
sender [ _ _ _ ] receiver
sender only blocks when buffer is full
ch := make(chan int, 2) // buffer holds 2
ch <- 1 // doesn't block
ch <- 2 // doesn't block
// ch <- 3 // would block here — buffer is full
fmt.Println(<-ch, <-ch) // 1 2

Closing and ranging

When we’re done sending, we close the channel. Receivers can then detect it.

ch := make(chan int, 3)
ch <- 1
ch <- 2
close(ch) // no more sends allowed after this

for v := range ch { // range keeps receiving until the channel is closed and drained
	fmt.Println(v) // 1, then 2
}

The two-value receive tells us if a channel is closed:

v, ok := <-ch // ok is false once the channel is closed and empty

The dangerous bits (interview favorites)

These three rules trip people up constantly:

  • Sending on a closed channel panics. Only the sender should close, never the receiver.
  • Receiving from a closed channel returns the zero value immediately (with ok == false). No panic.
  • A nil channel blocks forever. Sending or receiving on a nil channel never proceeds. Sometimes useful in select to disable a case, but usually a bug.
var ch chan int // nil channel
// <-ch // blocks forever — deadlock

Interview soundbite

A channel is a typed pipe for passing values between goroutines, embodying Go’s motto “share memory by communicating.” Unbuffered channels are a synchronous handshake — send blocks until a receiver shows up; buffered channels only block when full or empty. Remember the gotchas: closing then sending panics, only the sender closes, receiving from a closed channel gives the zero value, and a nil channel blocks forever.