Frontend Labs
⋯

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.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36