Problem Statement
At Netflix, a single user request fans out to 50+ microservices. If the user cancels their request (closes browser), we need to immediately stop all downstream work to save resources. Implement a service that properly propagates cancellation using Go's context package.
The Three Context Functions
// 1. Manual cancellation
ctx, cancel := context.WithCancel(parent)
// 2. Timeout (duration from now)
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
// 3. Deadline (absolute time)
ctx, cancel := context.WithDeadline(parent, time.Now().Add(5*time.Second))Production Example: HTTP Client with Timeout
package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
func fetchWithTimeout(url string, timeout time.Duration) (string, error) {
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() // Always call cancel to release resources
// Create request with context
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return "", err
}
// Make request
resp, err := http.DefaultClient.Do(req)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return "", fmt.Errorf("request timed out after %v", timeout)
}
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
return string(body), err
}
func main() {
result, err := fetchWithTimeout("https://api.example.com/slow", 2*time.Second)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
}Cancellation Propagation Pattern
func processOrder(ctx context.Context, orderID string) error {
// Check for cancellation before expensive operations
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Step 1: Validate inventory
if err := validateInventory(ctx, orderID); err != nil {
return err
}
// Step 2: Process payment (most expensive)
if err := processPayment(ctx, orderID); err != nil {
return err
}
// Step 3: Ship order
return shipOrder(ctx, orderID)
}
func validateInventory(ctx context.Context, orderID string) error {
// Long-running loop should check context periodically
for _, item := range getOrderItems(orderID) {
select {
case <-ctx.Done():
return ctx.Err()
default:
checkStock(item)
}
}
return nil
}Context Values: Use Sparingly
// Good: Request-scoped values like trace IDs
type ctxKey string
const traceIDKey ctxKey = "traceID"
func WithTraceID(ctx context.Context, traceID string) context.Context {
return context.WithValue(ctx, traceIDKey, traceID)
}
func GetTraceID(ctx context.Context) string {
if v := ctx.Value(traceIDKey); v != nil {
return v.(string)
}
return ""
}Anti-Patterns to Avoid
- ❌ Storing optional function parameters in context
- ❌ Storing database connections in context
- ❌ Storing the context inside a struct field
- ❌ Passing nil context (causes panic)
Follow-up Questions
- Does cancelling a child context cancel the parent?
- What's the difference between
context.Background()andcontext.TODO()? - How do you implement graceful shutdown using context?