# Why Channels Matter
Channels are the coordination backbone for concurrent Go programs: they move data, synchronize execution, and model ownership transfer between goroutines.
In production systems, channels usually power worker pools, streaming pipelines, fan-out/fan-in processing, and cancellation-aware background work.
# Unbuffered vs Buffered Channels
Unbuffered channels are a rendezvous point: send blocks until a receiver is ready. Buffered channels allow temporary decoupling, but still block when full.
syncCh := make(chan int) // unbuffered
queueCh := make(chan int, 3) // buffered
// syncCh send waits for receiver
// queueCh send waits only when buffer is full# Sending, Receiving, and Closing
Only the sender should close a channel. Receivers use comma-ok or range loops to detect completion.
v, ok := <-ch
if !ok {
fmt.Println("channel closed")
}
for v := range ch {
fmt.Println(v)
}# select, Timeouts, and Non-Blocking Ops
select waits on multiple channel operations, enables timeout behavior, and supports non-blocking checks via default.
select {
case v := <-ch:
fmt.Println("got", v)
case <-time.After(100 * time.Millisecond):
fmt.Println("timeout")
default:
fmt.Println("nothing ready right now")
}# Directional Channels and API Design
Directional channels document intent and prevent misuse at compile time. Use chan<- T for producers and <-chan T for consumers.
func producer(out chan<- int) { /* send only */ }
func consumer(in <-chan int) { /* receive only */ }# Pipeline and Cancellation Pattern
In multi-stage pipelines, combine channels with a done signal so goroutines can exit early instead of leaking when downstream stops consuming.
func square(done <-chan struct{}, in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
select {
case out <- n * n:
case <-done:
return
}
}
}()
return out
}# Practice Challenge
Build a worker pool with 3 workers consuming jobs and producing results; then add cancellation support so workers can stop cleanly when the caller no longer needs results.
package main
import "fmt"
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
results <- j * j
}
}⚡ Key Takeaways
- Use unbuffered channels for strict handoff, buffered channels for burst absorption
- Only senders should close channels; receivers detect closure via comma-ok or range
- Use select for timeouts, cancellation, and multiplexing
- Use directional channel types to enforce API intent
- Add cancellation paths in pipelines to avoid goroutine leaks