Golang Concurrency #9 - Context and Cancellations

Just a guy who loves to write code and watch anime.
Introduction
Context solves the problem: "How do I gracefully stop operations or set timeouts across multiple goroutines?"
Key Insight
Context provides cooperative cancellation -> you control when and how to check for cancellation, not abrupt force-killing.
Creating Contexts
1. WithTimeout -> Automatic cancellation after duration
// Cancel automatically after 3 seconds
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() // Always call cancel to free resources
2. WithCancel -> Manual cancellation
// Cancel manually when you want
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Later...
cancel() // Trigger cancellation
3. Background -> Root context
ctx := context.Background() // Never cancelled, starting point
How Cancellation Works
The cooperative pattern
func worker(ctx context.Context, name string) {
for { // Infinite loop -> YOU control when to exit
select {
case <-ctx.Done(): // "Did someone cancel me?"
fmt.Printf("%s: Got cancellation signal\n", name)
cleanup() // YOU decide what cleanup to do
fmt.Printf("%s: Cleaned up, exiting gracefully\n", name)
return // YOU decide when to exit
default: // "No cancellation signal, keep working"
fmt.Printf("%s: working...\n", name)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
// Pass same context to multiple workers
go worker(ctx, "Worker 1")
go worker(ctx, "Worker 2")
go worker(ctx, "Worker 3")
time.Sleep(2 * time.Second)
cancel() // Signal ALL workers to cancel
time.Sleep(1 * time.Second) // Give them time to clean up
}
What happens step by step
Workers start: Enter infinite
forloopSelect checks: "Is
ctx.Done()ready to receive?"If NOT cancelled: Goes to
default, does work, loops againWhen
cancel()called:ctx.Done()channel gets closedNext time workers hit select:
ctx.Done()case executesWorkers clean up: Do whatever cleanup they want
Workers exit: When THEY decide to
return
Different checking patterns
Pattern 1 -> Check every iteration
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
return // Check every loop iteration
default:
}
doWork()
}
}
Pattern 2 -> Check periodically
func worker(ctx context.Context) {
for i := 0; i < 1000000; i++ {
processItem(i)
// Only check cancellation every 1000 items
if i%1000 == 0 {
select {
case <-ctx.Done():
fmt.Printf("Cancelled at item %d\n", i)
return
default:
}
}
}
}
Pattern 3 -> Racing work vs cancellation
func fetchData(ctx context.Context) (string, error) {
result := make(chan string)
go func() {
time.Sleep(5 * time.Second) // Simulate slow work
result <- "data"
}()
select {
case data := <-result:
return data, nil // Work finished first
case <-ctx.Done():
return "", ctx.Err() // Cancellation won first
}
}
Timeout Example
func main() {
// Context automatically cancels after 2 seconds
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
result := make(chan string)
go slowWork(ctx, result)
select {
case data := <-result:
fmt.Println("Got result:", data)
case <-ctx.Done():
fmt.Println("Timeout! Work took too long")
fmt.Println("Error:", ctx.Err()) // context.DeadlineExceeded
}
}
func slowWork(ctx context.Context, result chan string) {
select {
case <-time.After(3 * time.Second): // Work takes 3 seconds
result <- "finished work"
case <-ctx.Done(): // But context times out after 2 seconds
fmt.Println("Work cancelled due to timeout")
return
}
}
Context Propagation
func handleRequest(ctx context.Context) {
// Pass context down the call chain
data := fetchFromAPI(ctx)
processData(ctx, data)
saveToDatabase(ctx, data)
}
func fetchFromAPI(ctx context.Context) string {
// This function also respects the same cancellation
select {
case <-time.After(1 * time.Second):
return "api data"
case <-ctx.Done():
return ""
}
}
Key Points
1. Cooperative Nature
Context doesn't force-kill anything. Your goroutines choose when to check and how to respond.
2. Graceful Shutdown
You can clean up, save state, close files, etc. before exiting.
3. Propagation
One context can control an entire tree of operations.
4. Channel Syntax
<-ctx.Done() receives from the Done() channel, which becomes ready when context is cancelled.
5. Always call cancel()
Even with timeout contexts, call cancel() to free resources.






