Select, Buffered Channels, and Context Propagation
Building on Day 1, we explore patterns to manage multiple goroutines.
switch for channels. It blocks until one of its cases can run. Essential for handling timeouts and multiple data streams.Implement a pipeline that fans out work to multiple goroutines and fans results back in, with cancellation support.
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, id int, jobs <-chan int, results chan<- int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d stopping\n", id)
return
case j, ok := <-jobs:
if !ok {
return
}
fmt.Printf("Worker %d processing %d\n", id, j)
time.Sleep(500 * time.Millisecond)
results <- j * j
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
jobs := make(chan int, 10)
results := make(chan int, 10)
// Fan-Out
for w := 1; w <= 3; w++ {
go worker(ctx, w, jobs, results)
}
// Send jobs
go func() {
for j := 1; j <= 10; j++ {
jobs <- j
}
close(jobs)
}()
// Fan-In (simplified for demo)
// In production, use a separate goroutine + WaitGroup to close results
for i := 0; i < 10; i++ {
select {
case res := <-results:
fmt.Println("Result:", res)
case <-ctx.Done():
fmt.Println("Main: Context timeout!")
return
}
}
}
Answer: Ensure every goroutine has a defined exit condition. Common strategies include using a done channel, passing a context.Context that gets cancelled, or ensuring the channel it reads from is closed.
Answer: Functionally they are the same (empty contexts). Semantically, `Background()` is the root of a context tree (main, init, tests), while `TODO()` is a placeholder when you're unsure which context to use or it hasn't been passed down yet.
Answer: Use them when you want to decouple the sender and receiver to avoid blocking on every send, or to limit throughput (e.g., a semaphore). Be careful not to use them just to mask a race condition or deadlock.