context Package — Cancellation, Timeouts, and Value Propagation
In real server programs, multiple goroutines collaborate to handle a single request. When a client disconnects or a timeout occurs, all related goroutines must be cancelled. The context package propagates cancellation signals, timeouts, and request-scoped values throughout a goroutine tree.
What Is context?
type Context interface {
Deadline() (deadline time.Time, ok bool) // Deadline time
Done() <-chan struct{} // Channel closed on cancellation
Err() error // Cancellation reason (Canceled/DeadlineExceeded)
Value(key any) any // Values stored in context
}
context Creation Functions
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 1. Root context (non-cancellable)
ctx := context.Background() // Top-level, no cancellation
// ctx := context.TODO() // When context type is undecided
// 2. Cancellable context
cancelCtx, cancel := context.WithCancel(ctx)
defer cancel() // Always call cancel (prevents resource leaks)
// 3. Timeout context
timeoutCtx, cancelTimeout := context.WithTimeout(ctx, 5*time.Second)
defer cancelTimeout()
// 4. Deadline context
deadline := time.Now().Add(10 * time.Second)
deadlineCtx, cancelDeadline := context.WithDeadline(ctx, deadline)
defer cancelDeadline()
// 5. Value context
valueCtx := context.WithValue(ctx, "userID", "user-123")
fmt.Println("cancelCtx:", cancelCtx)
fmt.Println("timeoutCtx:", timeoutCtx)
fmt.Println("deadlineCtx:", deadlineCtx)
fmt.Println("value:", valueCtx.Value("userID"))
}
WithCancel — Manual Cancellation
package main
import (
"context"
"fmt"
"time"
)
func doWork(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d cancelled: %v\n", id, ctx.Err())
return
default:
fmt.Printf("Worker %d working...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
// Start multiple workers
for i := 1; i <= 3; i++ {
go doWork(ctx, i)
}
// Cancel all workers after 2 seconds
time.Sleep(2 * time.Second)
fmt.Println("Sending cancel signal")
cancel() // Propagates cancellation to all child contexts
time.Sleep(100 * time.Millisecond)
fmt.Println("Main exiting")
}
WithTimeout — Time Limit
Use for HTTP requests, DB queries, and other external operations that need a time limit.
package main
import (
"context"
"fmt"
"time"
)
// Simulate a DB query
func queryDatabase(ctx context.Context, query string) (string, error) {
resultCh := make(chan string, 1)
go func() {
time.Sleep(3 * time.Second) // Simulate slow query
resultCh <- "query result"
}()
select {
case result := <-resultCh:
return result, nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func handleRequest(timeout time.Duration) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
result, err := queryDatabase(ctx, "SELECT * FROM users")
if err != nil {
fmt.Printf("Error (timeout %v): %v\n", timeout, err)
return
}
fmt.Println("Result:", result)
}
func main() {
handleRequest(1 * time.Second) // Timeout
handleRequest(5 * time.Second) // Success
}
// Output:
// Error (timeout 1s): context deadline exceeded
// Result: query result
WithDeadline — Absolute Time Deadline
package main
import (
"context"
"fmt"
"time"
)
func processWithDeadline() {
// Deadline at tomorrow midnight
tomorrow := time.Now().Add(24 * time.Hour)
ctx, cancel := context.WithDeadline(context.Background(), tomorrow)
defer cancel()
// Check current deadline
if deadline, ok := ctx.Deadline(); ok {
fmt.Printf("Deadline: %v\n", deadline.Format("2006-01-02 15:04:05"))
fmt.Printf("Time remaining: %v\n", time.Until(deadline).Round(time.Second))
}
// Short deadline for actual testing
shortCtx, shortCancel := context.WithDeadline(context.Background(),
time.Now().Add(100*time.Millisecond))
defer shortCancel()
select {
case <-time.After(500 * time.Millisecond):
fmt.Println("Done")
case <-shortCtx.Done():
fmt.Println("Deadline exceeded:", shortCtx.Err())
}
}
func main() {
processWithDeadline()
}
WithValue — Request-Scoped Value Propagation
Carry HTTP request IDs, user authentication info, etc. through context.
package main
import (
"context"
"fmt"
)
// Define type-safe keys (prevents string key collisions)
type contextKey string
const (
userIDKey contextKey = "userID"
requestIDKey contextKey = "requestID"
)
func middleware(ctx context.Context, userID, requestID string) context.Context {
ctx = context.WithValue(ctx, userIDKey, userID)
ctx = context.WithValue(ctx, requestIDKey, requestID)
return ctx
}
func serviceLayer(ctx context.Context) {
userID, ok := ctx.Value(userIDKey).(string)
if !ok {
fmt.Println("No user ID")
return
}
requestID := ctx.Value(requestIDKey).(string)
fmt.Printf("Service layer — user: %s, request ID: %s\n", userID, requestID)
}
func repositoryLayer(ctx context.Context) {
userID := ctx.Value(userIDKey).(string)
fmt.Printf("Repository layer — querying data for user %s\n", userID)
}
func main() {
ctx := context.Background()
ctx = middleware(ctx, "user-123", "req-456")
// Context propagates automatically
serviceLayer(ctx)
repositoryLayer(ctx)
}
WithValue notes:
- Only store request-scoped data (use function args for anything else)
- Use custom type keys instead of
string(prevents collisions) - Don't store many values — wrap in a struct if needed
Context Tree — Cancellation Propagation
Contexts form a tree. When a parent is cancelled, all children are cancelled.
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, name string, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("%s exiting: %v\n", name, ctx.Err())
return
case <-time.After(300 * time.Millisecond):
fmt.Printf("%s working...\n", name)
}
}
}
func main() {
// Root context
rootCtx, rootCancel := context.WithCancel(context.Background())
defer rootCancel()
// Child contexts
child1Ctx, child1Cancel := context.WithCancel(rootCtx)
defer child1Cancel()
child2Ctx, _ := context.WithTimeout(rootCtx, 2*time.Second)
// child2 auto-cancels after 2 seconds
var wg sync.WaitGroup
wg.Add(3)
go worker(rootCtx, "root-worker", &wg)
go worker(child1Ctx, "child1-worker", &wg)
go worker(child2Ctx, "child2-worker", &wg)
time.Sleep(1 * time.Second)
fmt.Println("Cancelling child1")
child1Cancel() // Only cancels child1
time.Sleep(1500 * time.Millisecond)
fmt.Println("Cancelling root (stops all)")
rootCancel() // Cancels root → also stops root-worker and child2-worker
wg.Wait()
}
Real-World Example — HTTP Server Request Handling
package main
import (
"context"
"fmt"
"net/http"
"time"
)
type contextKey string
const requestIDKey contextKey = "requestID"
// Request ID middleware
func requestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = fmt.Sprintf("req-%d", time.Now().UnixNano())
}
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func slowHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
requestID := ctx.Value(requestIDKey).(string)
select {
case <-time.After(5 * time.Second):
fmt.Fprintf(w, "Done! (request ID: %s)", requestID)
case <-ctx.Done():
// ctx.Done() closes automatically when client disconnects
fmt.Printf("Request cancelled (ID: %s): %v\n", requestID, ctx.Err())
http.Error(w, "Request cancelled", http.StatusRequestTimeout)
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/slow", slowHandler)
handler := requestIDMiddleware(mux)
fmt.Println("Server starting: :8080")
http.ListenAndServe(":8080", handler)
}
Key Takeaways
| Function | Purpose |
|---|---|
context.Background() | Root context (top-level) |
context.WithCancel(ctx) | Manual cancellation |
context.WithTimeout(ctx, d) | Duration-based timeout |
context.WithDeadline(ctx, t) | Absolute time deadline |
context.WithValue(ctx, k, v) | Request-scoped value propagation |
- Always call
cancel()— usedefer cancel()to prevent resource leaks - Pass ctx as first argument— conventional as the first function parameter
- Parent cancellation cancels children— tree structure
- Use custom type keys with WithValue— prevents string key collisions