RECIPE / CONCURRENCY

Go channels primer

Channels are Go's typed conduits for passing values between goroutines. They synchronize execution, communicate state, and replace the need for explicit locks in most pipeline workloads. This recipe walks through three patterns you will reach for every day.

1.Unbuffered vs buffered

An unbuffered channel forces sender and receiver to rendezvous: the send blocks until someone reads. A buffered channel decouples them up to its capacity, letting the producer run ahead until the buffer is full. Pick unbuffered when ordering matters and buffered when throughput does.

ch := make(chan int)        // unbuffered: hand-off
buf := make(chan int, 16)   // buffered: queue up to 16

go func() { ch <- 42 }()    // blocks until received
v := <-ch                   // receives 42

2.Close to signal done

Closing a channel broadcasts "no more values" to every receiver. Ranging over a closed channel drains buffered values then exits cleanly. Only the sender should close, and only once — closing twice or sending to a closed channel panics.

jobs := make(chan int, 3)
go func() {
    defer close(jobs)
    for i := 0; i < 3; i++ { jobs <- i }
}()
for j := range jobs { fmt.Println(j) }

3.Select for multiplexing

select waits on multiple channel operations and runs the first one ready. Combine it with time.After for timeouts or a default branch for non-blocking checks.

select {
case v := <-results:
    handle(v)
case <-time.After(2 * time.Second):
    return errors.New("timeout")
case <-ctx.Done():
    return ctx.Err()
}