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



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:

GO
// 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

GO
// 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):

TEXT
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

GO
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
💡 Tip: 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

GO
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)
}
▶ Try it Yourself

▶ Example: Cascading Cancellation

GO
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)
}
▶ Try it Yourself
100%
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
🔥 Common Mistake: 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

GO
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)
}
▶ Try it Yourself

▶ Example: WithTimeout vs. WithDeadline

GO
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())
}
▶ Try it Yourself

(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

GO
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")
}
▶ Try it Yourself
🔥 Common Mistake: The 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

GO
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

GO
// 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)
    }
}
💡 Tip: The 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

Q When should Context be used?
A In any scenario requiring timeout control, manual cancellation, or passing values at the request level. This includes: HTTP request processing, database queries, RPC calls, and scheduled tasks. Do not use Context in pure computational functions that do not require cancellation or timeout control.
Q What is the difference between Background() and TODO()?
A Both are root Contexts that can never be canceled, but they have different meanings: 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.
Q Which should I choose, WithTimeout or WithDeadline?
A In most cases, use 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.
Q Can Context be canceled multiple times?
A Cancellation can only be triggered once—repeated calls to cancel() are safe (the second and subsequent calls are no-ops). All goroutines listening for Done() will only receive the signal once.
Q What happens when a parent cancellation and a timeout occur simultaneously?
A The two are triggered independently, and the one that occurs first takes effect. If the parent Context is manually canceled, all child Contexts immediately receive a Done() signal, even if the child Contexts' timeouts have not yet expired. This ensures the shortest path for cascading cancellations.
Q Can the value passed to WithValue be modified?
A No. The value of 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.
Q What types of data is Context suitable for storing?
A It is only suitable for storing request-level metadata: TraceID, RequestID, UserID, and authentication tokens. It is not suitable for storing business parameters (such as price or quantity), nor is it suitable for storing database connections or configurations—those should be passed via dependency injection.

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty ⭐): Write a function FetchWithTimeout(ctx, url string, timeout time.Duration) that uses context.WithTimeout to control the timeout for HTTP requests. Once the timeout expires, the underlying HTTP request should be automatically canceled.

  2. 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 cancel function); (3) Batch cancellation (WithCancel chaining); (4) All tasks share a single root Context.

  3. Challenge (Difficulty ⭐⭐⭐): Simulate a distributed tracing system. Requirements: (1) Pass the TraceID using WithValue across 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. Use Deadline() from Context to calculate the remaining time.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏