16. Channels: pipelines, select, and the closing convention
16.1 Unbuffered vs. buffered
An unbuffered channel (make(chan int)) synchronizes sender and
receiver — a send blocks until a receive is ready, and vice versa. A
buffered channel (make(chan int, 10)) lets sends proceed without a
waiting receiver, up to the buffer size, decoupling producer and
consumer speed:
unbuffered := make(chan int) // hand-off, rendezvous
buffered := make(chan int, 10) // queue with capacity 10
16.2 select
select waits on multiple channel operations at once, proceeding with
whichever is ready first (randomly, if several are ready simultaneously):
select {
case v := <-ch1:
fmt.Println("from ch1:", v)
case ch2 <- 42:
fmt.Println("sent to ch2")
case <-time.After(2 * time.Second):
fmt.Println("timed out")
default:
fmt.Println("nothing ready, non-blocking")
}
16.3 The closing convention
- Only the sender should close a channel — never the receiver.
- Closing signals "no more values are coming"; a receive on a closed
channel returns immediately with the zero value and
ok == false. - Sending on a closed channel panics; closing an already-closed channel panics.
func producer(out chan<- int) {
defer close(out) // sender closes when done
for i := 0; i < 5; i++ {
out <- i
}
}
func consumer(in <-chan int) {
for v := range in { // range exits cleanly when in is closed
fmt.Println(v)
}
}
Directional channel types (chan<- int send-only, <-chan int
receive-only) in function signatures are cheap, compiler-enforced
documentation of intent — use them whenever a function only needs one
direction.
16.4 Pipelines
Channels compose into pipelines, each stage a goroutine reading from an input channel and writing to an output channel:
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
for result := range square(generate(1, 2, 3, 4)) {
fmt.Println(result) // 1, 4, 9, 16
}
16.5 Real-world example
Day 13's job queue — one producer, five workers, one results channel — is the buffered-channel worker-pool pattern used in many services, from image resizing to batch email senders. Chapter 25 builds the fuller version with cancellation and timeouts.