Channels & select
A channel is a typed conduit through which goroutines send and receive values. Go's concurrency mantra: "Don't communicate by sharing memory; share memory by communicating." Channels are how you do the latter.
Learning objectives
- Create, send, receive, and close channels.
- Distinguish buffered from unbuffered channels.
- Range over a channel until it's closed.
- Use
selectto wait on multiple channels and implement timeouts. - Diagnose and avoid deadlocks.
Creating a channel
ch := make(chan int) // unbuffered
buf := make(chan int, 10) // buffered, capacity 10
The zero value of a channel is nil; sends and receives
on a nil channel block forever. Always use make.
Sending & receiving
ch := make(chan string)
go func() {
ch <- "hello" // send
}()
msg := <-ch // receive (blocks until value available)
fmt.Println(msg)
An unbuffered channel synchronizes: the sender blocks until a receiver is ready, and vice versa.
Buffered channels
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3 // no blocking, buffer has room
// ch <- 4 // THIS would block until a receive happens
fmt.Println(<-ch, <-ch, <-ch) // 1 2 3
Buffers decouple producer and consumer timing. Use sparingly; a big buffer often indicates a design smell.
Closing
close(ch)
Only the sender should close. Receivers get the zero value and an "ok" of false:
v, ok := <-ch
if !ok { fmt.Println("closed") }
Sending to a closed channel panics. Closing an already-closed channel panics. Closing a nil channel panics. In short: be thoughtful about who closes.
range over a channel
ch := make(chan int)
go func() {
for i := 1; i <= 3; i++ {
ch <- i
}
close(ch) // ← signals the range loop to exit
}()
for v := range ch {
fmt.Println(v)
}
The range loop exits when the channel is closed and drained.
Channel direction
You can constrain a channel parameter to be send-only or receive-only , a powerful safety mechanism:
func producer(out chan<- int) { out <- 1 } // send-only
func consumer(in <-chan int) int{ return <-in } // receive-only
ch := make(chan int, 1)
go producer(ch)
fmt.Println(consumer(ch))
select
select waits on multiple channel operations, whichever
is ready first wins:
select {
case v := <-ch1:
fmt.Println("from ch1:", v)
case v := <-ch2:
fmt.Println("from ch2:", v)
case ch3 <- 42:
fmt.Println("sent to ch3")
default:
fmt.Println("no channel ready") // optional; makes select non-blocking
}
Timeouts
select {
case v := <-ch:
fmt.Println("got:", v)
case <-time.After(2 * time.Second):
fmt.Println("timed out")
}
time.After returns a channel that fires once after the
given duration. Combined with select, you get clean
timeouts. (For more sophisticated cancellation, see Chapter 26 on
context.)
Fan-in & fan-out
Fan-out: many workers consuming from one channel.
jobs := make(chan int, 100)
var wg sync.WaitGroup
// 3 workers fan-out from jobs
for w := 0; w < 3; w++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := range jobs {
fmt.Printf("worker %d: %d\n", id, j)
}
}(w)
}
for i := 1; i <= 10; i++ { jobs <- i }
close(jobs)
wg.Wait()
Fan-in: merging multiple input channels into one.
func merge(a, b <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for v := range a { out <- v }
for v := range b { out <- v }
}()
return out
}
Deadlocks
If every goroutine is blocked waiting, Go detects the deadlock and panics with a helpful message. Common causes:
- Sending to an unbuffered channel with no receiver.
- Receiving from a channel no one sends on.
- Forgetting to
closebefore arange.
ch := make(chan int)
ch <- 1 // fatal error: all goroutines are asleep - deadlock!
Check your understanding
Practice exercises
Worker pool
Write a worker pool: N worker goroutines consume from a jobs channel, compute each job's result, and push it to a results channel. The main goroutine feeds jobs and drains results.
Show solution
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for j := range jobs {
results <- j * j // square as a placeholder for "work"
}
}
func main() {
jobs := make(chan int, 10)
results := make(chan int, 10)
var wg sync.WaitGroup
// start 3 workers
for w := 1; w <= 3; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// feed
for j := 1; j <= 5; j++ { jobs <- j }
close(jobs)
// close results when all workers finish
go func() { wg.Wait(); close(results) }()
for r := range results {
fmt.Println(r)
}
}
Further reading
Channels mastered.