Go Context
Context is the cornerstone of concurrency control in Go—it allows timeouts, cancellations, and values to be passed gracefully through the goroutine call chain.
When your service needs to handle dozens of external dependencies simultaneously (each with its own timeout and cancellation logic), how can you manage them in a consistent way? In this lesson, you'll learn how to use the Go context package comprehensively.
1. You will learn
context.Background()andcontext.TODO()context.WithCancelManual cancellationcontext.WithTimeoutautomatically cancels after a timeoutcontext.WithDeadlinedeadline cancellationcontext.WithValue: Passing values at the طلب level- Context: Chain Passing Rules
- Hands-On: Preventing Cascading Timeouts in Microservice Chains
2. A True Story of a Backend Engineer
(1) Pain Point: If one upstream component times out, the entire system crashes
Xiaoli is an engineer on the order system. She is responsible for a REST API that depends on three downstream services:
"The order placement API called three services: the inventory service, the payment service, and the notification service. One day, the inventory service took 20 seconds to respond, causing my service's memory usage to skyrocket as all the goroutines were waiting for it—and requests from other users couldn't get through either. My boss asked, 'Why is the order page completely down?'"
Problem Analysis:
// Bad code: no timeout control
func PlaceOrder(ctx context.Context, order Order) error {
// If InventoryCheck hangs for 30 seconds, the goroutine waits needlessly for 30 seconds
ok, err := InventoryCheck(ctx, order.Items)
if err != nil {
return err
}
// If the payment service times out, 30 seconds have already been wasted; the user has long since given up
err = Charge(ctx, order.Total)
if err != nil {
return err
}
// Notification service also hangs... goroutine leak reaches the limit → OOM
return Notify(ctx, order.UserID)
}
(2) The Go Solution: Context
// context_demo.go
package main
import (
"context"
"fmt"
"time"
)
func main() {
// Root Context
root := context.Background()
// Wrap with WithTimeout: 2-second timeout
ctx, cancel := context.WithTimeout(root, 2*time.Second)
defer cancel() // Ensure resources are released
result := PlaceOrder(ctx, "order-123")
fmt.Println(result)
}
func PlaceOrder(ctx context.Context, orderID سلسلة) سلسلة {
// Check if Context is already canceled
select {
case <-ctx.Done():
return fmt.Sprintf("Canceled: %v", ctx.Err())
default:
}
// Assign a shorter timeout to each downstream call
checkCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
ok := InventoryCheck(checkCtx, orderID)
if !ok {
return "Insufficient inventory"
}
return "Order placed successfully"
}
func InventoryCheck(ctx context.Context, orderID سلسلة) bool {
// Simulate a slow call
select {
case <-time.After(500 * time.Millisecond):
return true
case <-ctx.Done():
fmt.Printf("InventoryCheck canceled: %v\n", ctx.Err())
return false
}
}
Output (Normal):
Order placed successfully
(3) Benefits: With Context vs. Without Context
| Situation | Result |
|---|---|
| No timeout control | Goroutine leak, system OOM |
| Manual time.After checks | Code is scattered; each دالة has its own timeout |
| Unified Context Control | If the parent Context is canceled, all child Contexts are canceled in a cascading manner |
3. Root Context
(1) Background and TODO
package main
import (
"context"
"fmt"
)
func main() {
// Background(): root Context, never canceled
// Used in main functions, initialization, top-level requests
ctx := context.Background()
fmt.Printf("Background: %v\n", ctx)
// TODO(): placeholder when unsure which Context to use
// Marks code that has not yet been integrated with Context and needs refactoring
todo := context.TODO()
fmt.Printf("TODO: %v\n", todo)
}
(2) Background vs TODO
| Context | Purpose | Will it be canceled? |
|---|---|---|
Background() |
Root node, starting point for all Contexts | Never |
TODO() |
Placeholder indicating that the code has not yet been integrated with Context | Never |
context.Background() is the root node of all Context trees and is never canceled. context.TODO() is used to mark code that has not yet been integrated into the Context chain—you should replace it with an appropriate Context as soon as possible.
4. context.WithCancel: Manual cancellation
▶ Example: Manually canceling a goroutine
package main
import (
"context"
"fmt"
"time"
)
func Worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d stopped: %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())
go Worker(ctx, 1)
go Worker(ctx, 2)
time.Sleep(2 * time.Second)
fmt.Println("Main goroutine initiating cancellation...")
cancel() // Tell all Workers to stop
// Wait for goroutines to exit
time.Sleep(500 * time.Millisecond)
}
▶ Example: Cascading Cancellation
package main
import (
"context"
"fmt"
"time"
)
func handler(ctx context.Context) {
// Child Context inherits parent Context
childCtx, cancel := context.WithCancel(ctx)
defer cancel()
go subTask(childCtx, "task-1")
go subTask(childCtx, "task-2")
// Parent Context canceled → Child Context automatically canceled
select {
case <-time.After(1 * time.Second):
fmt.Println("Handler complete")
case <-ctx.Done():
fmt.Println("Handler canceled")
}
}
func subTask(ctx context.Context, name string) {
select {
case <-time.After(3 * time.Second):
fmt.Printf("%s complete\n", name)
case <-ctx.Done():
fmt.Printf("%s canceled: %v\n", name, ctx.Err())
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go handler(ctx)
time.Sleep(500 * time.Millisecond)
cancel() // Cancel → handler → subTask all cascadingly canceled
time.Sleep(1 * time.Second)
}
sequenceDiagram
participant Main as main()
participant H as handler
participant ST1 as subTask-1
participant ST2 as subTask-2
Main->>H: WithCancel
H->>ST1: WithCancel
H->>ST2: WithCancel
Note over Main,ST2: Normal execution
Main->>Main: cancel()
Main-->>H: ctx.Done()
H-->>ST1: ctx.Done()
H-->>ST2: ctx.Done()
Note over Main,ST2: All cascadingly canceled
cancel() must be called. Even if you use WithTimeout, you must defer cancel(). Otherwise, the Context's resources (timers, goroutines) will not be released. Rule: When creating a WithCancel, WithTimeout, or WithDeadline → immediately defer cancel().
5. context.WithTimeout: Automatic cancellation upon timeout
▶ Example: Timeout Control
package main
import (
"context"
"fmt"
"time"
)
func callExternalAPI(ctx context.Context, name string, delay time.Duration) (string, error) {
select {
case <-time.After(delay):
return fmt.Sprintf("%s response", name), nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func main() {
// 1-second timeout
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
// Call two downstream services
result1 := make(chan string, 1)
result2 := make(chan string, 1)
go func() {
r, err := callExternalAPI(ctx, "ServiceA", 500*time.Millisecond)
if err != nil {
result1 <- fmt.Sprintf("ServiceA failed: %v", err)
return
}
result1 <- r
}()
go func() {
r, err := callExternalAPI(ctx, "ServiceB", 1500*time.Millisecond)
if err != nil {
result2 <- fmt.Sprintf("ServiceB failed: %v", err)
return
}
result2 <- r
}()
fmt.Println(<-result1) // ServiceA response (500ms < 1s timeout)
fmt.Println(<-result2) // ServiceB failed: context deadline exceeded (1500ms > 1s)
}
▶ Example: WithTimeout vs. WithDeadline
package main
import (
"context"
"fmt"
"time"
)
func main() {
// WithTimeout: timeout 2 seconds from now
timeoutCtx, cancel1 := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel1()
// WithDeadline: specify absolute time
deadline := time.Now().Add(2 * time.Second)
deadlineCtx, cancel2 := context.WithDeadline(context.Background(), deadline)
defer cancel2()
// Both have the same effect
fmt.Printf("timeoutCtx deadline: %v\n", timeoutCtx.Deadline())
fmt.Printf("deadlineCtx deadline: %v\n", deadlineCtx.Deadline())
}
(3) WithTimeout vs WithDeadline
| Method | Parameters | Purpose |
|---|---|---|
WithTimeout(parent, 2*time.Second) |
Relative time | Most commonly used: "Wait up to 2 seconds" |
WithDeadline(parent, time.Time) |
Absolute time | Specifies a deadline: "Complete by 15:30" |
6. Passing Values at the Request Level Using context.WithValue
▶ Example: WithValue
package main
import (
"context"
"fmt"
)
// Custom key type (avoids conflicts)
type contextKey string
const (
UserIDKey contextKey = "user_id"
TraceIDKey contextKey = "trace_id"
RequestIDKey contextKey = "request_id"
)
func middleware(ctx context.Context) context.Context {
// Get trace ID from request header
ctx = context.WithValue(ctx, TraceIDKey, "trace-123")
ctx = context.WithValue(ctx, RequestIDKey, "req-456")
return ctx
}
func handler(ctx context.Context, userID string) {
ctx = context.WithValue(ctx, UserIDKey, userID)
// Pass to business layer
service(ctx)
}
func service(ctx context.Context) {
// Retrieve values from Context
userID := ctx.Value(UserIDKey).(string)
traceID := ctx.Value(TraceIDKey).(string)
requestID := ctx.Value(RequestIDKey).(string)
fmt.Printf("Processing request: user=%s, trace=%s, request=%s\n",
userID, traceID, requestID)
}
func main() {
ctx := context.Background()
ctx = middleware(ctx)
handler(ctx, "user-007")
}
key in context.WithValue must be a custom type; you cannot use a string directly. If two packages both use the string "user_id" as the key, a conflict will occur. Using the custom type type contextKey string is best practice.
(2) Use Cases for WithValue
| Scenario | Recommended | Not Recommended |
|---|---|---|
| TraceID / RequestID | ✅ Passed via WithValue | ❌ Global variable |
| Authentication Token | ✅ Passed by WithValue | ❌ Function parameter |
| Database connection | ❌ Obtained via dependency injection | ❌ WithValue |
| Business Parameter | ❌ Explicit Parameter | ❌ WithValue Implicit Passing |
7. Context Chaining Rules
package main
import (
"context"
"fmt"
"time"
)
type contextKey string
func main() {
root := context.Background()
// Chaining: WithCancel → WithTimeout → WithValue
ctx1, cancel1 := context.WithCancel(root)
defer cancel1()
ctx2, cancel2 := context.WithTimeout(ctx1, 2*time.Second)
defer cancel2()
ctx3 := context.WithValue(ctx2, contextKey("trace"), "trace-007")
// ctx3 inherits ctx1's cancellation + ctx2's timeout + ctx3's value
fmt.Printf("ctx3 deadline: %v\n", ctx3.Deadline())
fmt.Printf("ctx3 value: %v\n", ctx3.Value(contextKey("trace")))
// Cancel ctx1 first → ctx2 and ctx3 both receive the cancellation signal
cancel1()
time.Sleep(10 * time.Millisecond)
fmt.Printf("ctx2 err: %v\n", ctx2.Err())
fmt.Printf("ctx3 err: %v\n", ctx3.Err())
}
(1) Context Passing Rules
| Rule | Description |
|---|---|
| First parameter | The first parameter in a function signature is always ctx context.Context |
| Do not store in a struct | Do not store Context in a struct field; instead, pass it as a parameter |
| Passing Between Functions | Each function that needs to be aware of cancellation or timeouts receives a Context |
| Immutable chain | Each call to WithCancel/WithTimeout/WithValue returns a new Context |
| Cascading Cancellation | Canceling a parent item → All child items are canceled; canceling a child item does not affect the parent |
8. Complete Example: Timeout Control for Microservice Tracing
// microservice_chain.go
package main
import (
"context"
"fmt"
"math/rand"
"time"
)
// ---------- Simulated downstream services ----------
// Inventory service
type InventoryService struct{}
func (s *InventoryService) Check(ctx context.Context, orderID string) (bool, error) {
// Simulate random delay
delay := time.Duration(rand.Intn(1500)) * time.Millisecond
select {
case <-time.After(delay):
return true, nil
case <-ctx.Done():
return false, ctx.Err()
}
}
// Payment service
type PaymentService struct{}
func (s *PaymentService) Charge(ctx context.Context, amount float64) (string, error) {
delay := time.Duration(rand.Intn(1500)) * time.Millisecond
select {
case <-time.After(delay):
return "pay-" + fmt.Sprintf("%d", time.Now().UnixNano()), nil
case <-ctx.Done():
return "", ctx.Err()
}
}
// Notification service
type NotificationService struct{}
func (s *NotificationService) Send(ctx context.Context, userID, message string) error {
delay := time.Duration(rand.Intn(1500)) * time.Millisecond
select {
case <-time.After(delay):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// ---------- Business layer ----------
type OrderService struct {
inventory *InventoryService
payment *PaymentService
notify *NotificationService
}
func NewOrderService() *OrderService {
return &OrderService{
inventory: &InventoryService{},
payment: &PaymentService{},
notify: &NotificationService{},
}
}
// PlaceOrder uses Context to control the entire chain's timeout
func (s *OrderService) PlaceOrder(ctx context.Context, userID, orderID string, amount float64) error {
// 1. Inventory check (wait at most 1 second)
invCtx, invCancel := context.WithTimeout(ctx, 1*time.Second)
defer invCancel()
ok, err := s.inventory.Check(invCtx, orderID)
if err != nil {
return fmt.Errorf("inventory check failed: %w", err)
}
if !ok {
return fmt.Errorf("insufficient inventory")
}
// 2. Payment (wait at most 2 seconds)
payCtx, payCancel := context.WithTimeout(ctx, 2*time.Second)
defer payCancel()
paymentID, err := s.payment.Charge(payCtx, amount)
if err != nil {
return fmt.Errorf("payment failed: %w", err)
}
// 3. Notification (wait at most 500ms)
notifyCtx, notifyCancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer notifyCancel()
err = s.notify.Send(notifyCtx, userID, "Order placed: "+orderID)
if err != nil {
// Notification failure does not affect the order (async log recording)
fmt.Printf("Notification failed (logged): %v\n", err)
}
fmt.Printf("Order placed successfully: user=%s, order=%s, payment=%s\n", userID, orderID, paymentID)
return nil
}
// ---------- Client ----------
func main() {
svc := NewOrderService()
// Overall request timeout of 3 seconds
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
err := svc.PlaceOrder(ctx, "user-007", "order-123", 99.99)
if err != nil {
fmt.Printf("Order failed: %v\n", err)
}
}
Err() method of Context returns one of two possible values: context.Canceled (manually canceled) or context.DeadlineExceeded (timed out). Within a chain, you can use errors.Is(err, context.DeadlineExceeded) to determine which type of cancellation occurred and decide on a retry strategy accordingly.
❓ FAQ
Background() and TODO()?Background() is the root you're actively using in your code; TODO() is a placeholder, indicating that this section of code has not yet been integrated into the Context and needs to be refactored as soon as possible.WithTimeout or WithDeadline?WithTimeout ("wait up to 2 seconds" is more intuitive). Use WithDeadline only when your timeout is an absolute time ("complete by 15:30:00"). WithTimeout internally calls WithDeadline.Context be canceled multiple times?cancel() are safe (the second and subsequent calls are no-ops). All goroutines listening for Done() will only receive the signal once.WithValue be modified?context.WithValue is immutable. What is referred to as "modification" essentially involves creating a new Context. A child Context's WithValue does not affect the parent Context. This ensures concurrency safety.📖 Summary
- Background() / TODO(): Two types of root contexts; cannot be canceled
- WithCancel: Manually control goroutine cancellation
- WithTimeout / WithDeadline: Automatic cancellation upon timeout
- WithValue: Request-level value passing (custom key types)
- Chain passing: The first argument is always Context
- Cascading cancellation: Parent cancellation → All child items canceled
cancel()must be called usingdefer- Hands-On: Preventing Cascading Timeouts in Microservice Chains
📝 Exercises
-
Basic Exercise (Difficulty ⭐): Write a function
FetchWithTimeout(ctx, url string, timeout time.Duration)that usescontext.WithTimeoutto control the timeout for HTTP requests. Once the timeout expires, the underlying HTTP request should be automatically canceled. -
Advanced Problem (Difficulty ⭐⭐): Implement a concurrently cancellable scheduled task manager. It should support: (1) registering multiple scheduled tasks (one per goroutine); (2) individual cancellation (by calling the
cancelfunction); (3) Batch cancellation (WithCancel chaining); (4) All tasks share a single root Context. -
Challenge (Difficulty ⭐⭐⭐): Simulate a distributed tracing system. Requirements: (1) Pass the TraceID using
WithValueacross three layers of function calls (API → Service → DB); (2) Each layer has its own timeout control (Service: 2s, DB: 500ms); (3) If a timeout occurs, the current layer is canceled but does not affect upstream layers; (4) Output the elapsed time and TraceID for each stage. UseDeadline()fromContextto calculate the remaining time.