Day 15 — context Package
Learning Objectives
- Propagate cancellation and deadlines through a call stack
- Understand why context is Go's answer to request-scoped data
- Build timeout chains: parent cancels children automatically
Key Concepts
Request arrives
└── ctx, cancel = context.WithTimeout(bg, 30s)
├── auth check ctx, _ = context.WithTimeout(ctx, 2s)
├── DB query ctx, _ = context.WithTimeout(ctx, 5s)
└── response context.WithValue(ctx, "request_id", "abc123")
If auth takes >2s: auth context times out → only auth cancels
If parent >30s: ALL children cancel simultaneously
// Creating contexts
ctx := context.Background() // root, never cancels
ctx := context.TODO() // placeholder during refactoring
// Cancellation
ctx, cancel := context.WithCancel(parent)
defer cancel() // ALWAYS defer cancel to avoid context leak
// Timeout
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
defer cancel()
// Deadline
ctx, cancel := context.WithDeadline(parent, time.Now().Add(5*time.Second))
defer cancel()
// Request-scoped values (use typed keys to avoid collisions)
type ctxKey string
const requestIDKey ctxKey = "request_id"
ctx = context.WithValue(ctx, requestIDKey, "abc-123")
id := ctx.Value(requestIDKey).(string)
// Check cancellation
select {
case <-ctx.Done():
return ctx.Err() // context.DeadlineExceeded or context.Canceled
default:
// not cancelled, proceed
}
Exercise: Multi-Stage Request Pipeline
package main
import (
"context"
"fmt"
"time"
)
type ctxKey string
const (
requestIDKey ctxKey = "request_id"
userIDKey ctxKey = "user_id"
)
// authenticate validates credentials — timeout: 2s
func authenticate(ctx context.Context, token string) (string, error) {
authCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
// Simulate DB lookup
done := make(chan string, 1)
go func() {
time.Sleep(50 * time.Millisecond) // fast auth
done <- "user:42"
}()
select {
case userID := <-done:
return userID, nil
case <-authCtx.Done():
return "", fmt.Errorf("authenticate: %w", authCtx.Err())
}
}
// fetchData retrieves data — timeout: 5s
func fetchData(ctx context.Context, userID string) (map[string]string, error) {
dataCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
done := make(chan map[string]string, 1)
go func() {
time.Sleep(100 * time.Millisecond)
done <- map[string]string{"name": "Alice", "role": "admin"}
}()
select {
case data := <-done:
return data, nil
case <-dataCtx.Done():
return nil, fmt.Errorf("fetchData: %w", dataCtx.Err())
}
}
func handleRequest(ctx context.Context, requestID, token string) error {
// Inject request ID into context — available to all downstream functions
ctx = context.WithValue(ctx, requestIDKey, requestID)
// Stage 1: auth
userID, err := authenticate(ctx, token)
if err != nil {
return fmt.Errorf("[%s] auth failed: %w", requestID, err)
}
ctx = context.WithValue(ctx, userIDKey, userID)
fmt.Printf("[%s] authenticated as %s\n", requestID, userID)
// Stage 2: fetch data
data, err := fetchData(ctx, userID)
if err != nil {
return fmt.Errorf("[%s] data fetch failed: %w", requestID, err)
}
fmt.Printf("[%s] data: %v\n", requestID, data)
return nil
}
func main() {
// Normal request — 30s budget, well within timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := handleRequest(ctx, "req-001", "valid-token"); err != nil {
fmt.Println("ERROR:", err)
}
// Tight deadline — will fail
tightCtx, tightCancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
defer tightCancel()
if err := handleRequest(tightCtx, "req-002", "valid-token"); err != nil {
fmt.Println("TIMEOUT:", err)
}
}
Real-World Context
Every HTTP handler in your API gateway receives a context from net/http. The context is cancelled when the client disconnects. Without checking ctx.Done(), your handler keeps running, consuming DB connections and CPU, even though no one will receive the response. Always propagate context.