Channels and Concurrency Patterns
Channel Review
Section titled “Channel Review”Unbuffered channels synchronize sender and receiver: both must be ready at the same time. Buffered Channels allow the sender to proceed up to the buffer capacity without a receiver.
unbuf := make(chan int) // blocks until receiver readybuf := make(chan int, 10) // sender can buffer up to 10Select
Section titled “Select”select allows a goroutine to wait on multiple channel operations simultaneously. It blocks until One of its cases can proceed, then executes that case:
ch1 := make(chan string)ch2 := make(chan string)
go func() { ch1 <- "from ch1" }()go func() { ch2 <- "from ch2" }()
select {case msg1 := <-ch1: fmt.Println(msg1)case msg2 := <-ch2: fmt.Println(msg2)}Select with Default
Section titled “Select with Default”A default case makes select non-blocking:
select {case msg := <-ch: fmt.Println("received:", msg)default: fmt.Println("no message available")}Select Timeout
Section titled “Select Timeout”Use time.After to implement timeouts:
select {case result := <-ch: fmt.Println("result:", result)case <-time.After(3 * time.Second): fmt.Println("timed out")}Select Loop
Section titled “Select Loop”select in a for loop handles multiple events over time:
for { select { case msg := <-ch: fmt.Println("received:", msg) case <-done: fmt.Println("done") return case <-time.After(1 * time.Second): fmt.Println("waiting...") }}Nil Channels
Section titled “Nil Channels”Sending to or receiving from a nil channel blocks forever. This is useful in select to Temporarily disable a case:
var ch1 chan int // nilvar ch2 = make(chan int, 1)
ch2 <- 42
select {case <-ch1: fmt.Println("ch1") // never selected (nil channel blocks)case v := <-ch2: fmt.Println("ch2:", v) // selected}Concurrency Patterns
Section titled “Concurrency Patterns”Fan-out, Fan-in
Section titled “Fan-out, Fan-in”Distribute work across multiple goroutines (fan-out), then collect results (fan-in):
func fanOut(in <-chan int, workers int) <-chan int { out := make(chan int) var wg sync.WaitGroup
for i := 0; i < workers; i++ { wg.Add(1) go func() { defer wg.Done() for v := range in { out <- process(v) } }() }
go func() { wg.Wait() close(out) }()
return out}
func fanIn(chs ...<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup
for _, ch := range chs { wg.Add(1) go func(c <-chan int) { defer wg.Done() for v := range c { out <- v } }(ch) }
go func() { wg.Wait() close(out) }()
return out}Pipeline
Section titled “Pipeline”A pipeline is a series of stages connected by channels, where each stage receives values from Upstream and sends values downstream:
func generator(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out}
func square(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n * n } close(out) }() return out}
func main() { for n := range square(square(generator(1, 2, 3, 4))) { fmt.Println(n) // 1, 16, 81, 256 }}Each stage runs in its own goroutine. Stages are connected only by channels. Cancellation requires Propagating a done signal through the pipeline.
Worker Pool
Section titled “Worker Pool”A fixed number of workers process jobs from a shared channel:
func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { results <- j * 2 }}
func main() { jobs := make(chan int, 100) results := make(chan int, 100)
for w := 1; w <= 3; w++ { go worker(w, jobs, results) }
for j := 1; j <= 9; j++ { jobs <- j } close(jobs)
for r := 1; r <= 9; r++ { fmt.Println(<-results) }}Bounded Parallelism
Section titled “Bounded Parallelism”Limit the number of concurrent goroutines processing a collection:
func processItems(items []Item, maxConcurrency int) { sem := make(chan struct{}, maxConcurrency) var wg sync.WaitGroup
for _, item := range items { wg.Add(1) sem <- struct{}{} // acquire semaphore go func(it Item) { defer wg.Done() defer func() { <-sem }() // release semaphore process(it) }(item) }
wg.Wait()}The context Package
Section titled “The context Package”The context package provides cancellation, deadlines, and request-scoped values across goroutine Boundaries. It is the standard mechanism for propagating cancellation signals in Go.
Context Types
Section titled “Context Types”| Type | Purpose |
|---|---|
context.Background | Root context, never cancelled |
context.TODO | Placeholder when context is not yet available |
context.WithCancel | Returns a cancelable context and cancel function |
context.WithTimeout | Returns a context that cancels after a deadline |
context.WithDeadline | Returns a context that cancels at a specific time |
context.WithValue | Returns a context carrying a key-value pair |
WithCancel
Section titled “WithCancel”func longRunning(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("cancelled:", ctx.Err()) return default: // do work } }}
func main() { ctx, cancel := context.WithCancel(context.Background()) go longRunning(ctx)
time.Sleep(2 * time.Second) cancel() // sends cancellation signal time.Sleep(100 * time.Millisecond)}WithTimeout
Section titled “WithTimeout”ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()
result, err := slowOperation(ctx)if err != nil { if ctx.Err() == context.DeadlineExceeded { fmt.Println("operation timed out") } return}WithValue
Section titled “WithValue”type key string
const requestIDKey key = "requestID"
ctx := context.WithValue(context.Background(), requestIDKey, "abc-123")
func handler(ctx context.Context) { id := ctx.Value(requestIDKey).(string) fmt.Println("request:", id)}Use context.WithValue sparingly and only for request-scoped data that flows through the call Chain. Do not use it as a global variable substitute.
Cancellation Propagation
Section titled “Cancellation Propagation”Contexts form a tree. Cancelling a parent cancels all its children:
parent, cancel := context.WithCancel(context.Background())child1, _ := context.WithCancel(parent)child2, _ := context.WithCancel(parent)
cancel() // cancels parent, child1, and child2This is how HTTP request cancellation works: the server creates a context per request, and when the Client disconnects, the context is cancelled, propagating to all goroutines handling that request.
Intuition
Section titled “Intuition”Channels are typed pipes with rendezvous semantics: Picture a pipe connecting two workers. An unbuffered channel is a pipe with zero storage — the sender must wait until the receiver grabs the part, like a relay race where the baton must be handed off face-to-face. A buffered channel is a pipe with a small shelf — the sender can drop off parts and walk away, as long as the shelf isn’t full. select is like a worker checking multiple bins simultaneously, picking up whichever has work available.
Why it matters: Channels and select enable composing concurrent behaviors without shared state. Pipelines, fan-out/fan-in, and worker pools are natural consequences of thinking in terms of data flowing through pipes.
The key insight: Channel semantics (blocking, buffering, select) enforce synchronization at the type level — deadlocks and race conditions become structural problems you can reason about visually.
Common Pitfalls
Section titled “Common Pitfalls”Forgetting to call the cancel function.
WithCancel``WithTimeoutAndWithDeadlinereturn a cancel function that must be called to release resources. Usedefer cancel().Not propagating context through function calls. If a function starts goroutines, accept a
context.Contextparameter so callers can cancel the work.Using
context.WithValuefor business logic data. Values in context are untyped. Prefer function parameters for structured data. Reserve context values for cross-cutting concerns (request IDs, tracing, auth tokens).Buffered channel deadlocks. If all goroutines are blocked sending to a channel and no goroutine is receiving (or vice versa), the program deadlocks. Ensure there is always at least one receiver for each sender.
Leaking goroutines. A goroutine that blocks forever on a channel read with no sender (or vice versa) is a goroutine leak. Use context cancellation to ensure goroutines can exit.
Closing a channel from the receiver. Only the sender should close a channel. If the receiver closes it, the sender may panic when trying to send.
Race conditions with
select. If multiple cases are ready,selectchooses uniformly at random. Do not assume a preferred order.
Summary
Section titled “Summary”This topic covers the core concepts of channels and concurrency patterns, including underlying theory, practical implementation, and key applications.
Key concepts include:
- core concepts and terminology
- algorithms and computational thinking
- practical implementation
- security and ethical considerations
- applications in the real world
Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Cross-References
Section titled “Cross-References”- Race Conditions: Mutex-based synchronization as an alternative to channel communication.
- Networking: Goroutine-based network servers using channel patterns.
- I/O: Stream processing with io.Reader/Writer in concurrent pipelines.