Skip to main content

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

FunctionPurpose
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()— use defer 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