Skip to content

Go Context — Cancellation, Deadlines, and Values with context.Context

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Go Context. We cover key concepts, practical examples, and best practices to help you master this topic.

Go context.Context propagates cancellation signals, deadlines, and request-scoped values across API boundaries and Goroutine hierarchies.

What You'll Learn

  • Creating contexts with Background and TODO
  • Context cancellation with cancel
  • Context deadlines and timeouts
  • Passing request-scoped values

Why It Matters

Context enables graceful shutdown and request lifecycle management. Docker uses context for build cancellation. Kubernetes uses context for request timeouts. DodaZIP uses context for file processing cancellation.

Real-World Use

HTTP request cancellation, database query timeouts, graceful shutdown, distributed tracing, request-scoped logging.

flowchart LR
    A["Context"] --> B["Background/TODO"]
    A --> C["WithCancel"]
    A --> D["WithDeadline"]
    A --> E["WithValue"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Basic Context with Cancel

func operation(ctx context.Context) {
    select {
    case <-time.After(2 * time.Second):
        fmt.Println("Operation completed")
    case <-ctx.Done():
        fmt.Println("Cancelled:", ctx.Err())
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    go operation(ctx)

    time.Sleep(1 * time.Second)
    cancel()

    time.Sleep(100 * time.Millisecond)
}

Context with Timeout

func slowOperation(ctx context.Context) (string, error) {
    ch := make(chan string, 1)

    go func() {
        time.Sleep(3 * time.Second)
        ch <- "result"
    }()

    select {
    case res := <-ch:
        return res, nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    result, err := slowOperation(ctx)
    if err != nil {
        fmt.Println("Error:", err) // context deadline exceeded
        return
    }
    fmt.Println(result)
}

HTTP Server with Context

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    result, err := processRequest(ctx)
    if err != nil {
        if errors.Is(err, context.Canceled) {
            log.Println("Request cancelled by client")
            return
        }
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    fmt.Fprintf(w, "Result: %s", result)
}

func processRequest(ctx context.Context) (string, error) {
    select {
    case <-time.After(2 * time.Second):
        return "done", nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

Context with Values

type contextKey string

const (
    UserIDKey contextKey = "user_id"
    TraceIDKey contextKey = "trace_id"
)

func WithUserID(ctx context.Context, userID string) context.Context {
    return context.WithValue(ctx, UserIDKey, userID)
}

func WithTraceID(ctx context.Context, traceID string) context.Context {
    return context.WithValue(ctx, TraceIDKey, traceID)
}

func GetUserID(ctx context.Context) (string, bool) {
    id, ok := ctx.Value(UserIDKey).(string)
    return id, ok
}

func main() {
    ctx := context.Background()
    ctx = WithTraceID(ctx, "abc-123")
    ctx = WithUserID(ctx, "user-42")

    if traceID, ok := ctx.Value(TraceIDKey).(string); ok {
        fmt.Println("Trace:", traceID)
    }
    if userID, ok := GetUserID(ctx); ok {
        fmt.Println("User:", userID)
    }
}

Database Query with Context

func queryDatabase(ctx context.Context) error {
    db, _ := sql.Open("postgres", "postgres://...")
    defer db.Close()

    ctxTimeout, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    row := db.QueryRowContext(ctxTimeout, "SELECT count(*) FROM users")
    var count int
    if err := row.Scan(&count); err != nil {
        return err
    }
    fmt.Println("Users:", count)
    return nil
}

Common Mistakes

1. Ignoring ctx.Done()

func handler(ctx context.Context) {
    // Must check ctx.Done() in long-running operations
    for {
        select {
        case <-ctx.Done():
            return
        default:
            // do work
        }
    }
}

2. Not Calling cancel

ctx, cancel := context.WithCancel(parent)
// defer cancel()  // Missing! Context leaks resources

3. Using Context for Optional Params

// Bad: Use context for optional parameters
// Good: Use explicit function parameters
ctx = context.WithValue(ctx, "page", 1)

4. Passing nil Context

func doSomething(ctx context.Context) {
    // Pass context.Background() or context.TODO()
    // Never pass nil
}

5. Storing Context in Structs

Context should be passed as a function parameter, not stored in structs.

Practice Questions

1. What is context.Background? The root context, never cancelled. Used in main() and top-level handlers.

2. What is context.TODO? A placeholder when you haven't decided which context to use. Should be replaced with real context.

3. How does WithTimeout work? Returns a context and cancel function. Context automatically cancels after the duration.

4. Can context values be concurrent-safe? Yes, context is immutable and safe for concurrent access.

Challenge: Write an HTTP handler that uses context timeout to limit database queries to 100ms.

Solution
func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    queryCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
    defer cancel()

    result := make(chan string, 1)
    go func() {
        result <- queryDatabase()
    }()

    select {
    case res := <-result:
        fmt.Fprint(w, res)
    case <-queryCtx.Done():
        http.Error(w, "query timeout", http.StatusGatewayTimeout)
    }
}

FAQ

{{< faq question="Should I pass context as first parameter?" >}} Yes, convention is context as the first parameter, typically named ctx. For example: func DoSomething(ctx context.Context, arg string). {{< /faq >}}

{{< faq question="What happens if context is cancelled while a goroutine is working?" >}} The ctx.Done() channel closes. Goroutines should select on ctx.Done() to detect cancellation and clean up. {{< /faq >}}

{{< faq question="Can I add multiple values to context?" >}} Yes, by wrapping: ctx = context.WithValue(ctx, key1, val1) then ctx = context.WithValue(ctx, key2, val2). {{< /faq >}}

{{< faq question="When does context deadline exceeded occur?" >}} When using WithTimeout or WithDeadline and the time expires before the operation completes. {{< /faq >}}

{{< faq question="What is the difference between Background and TODO?" >}} Background is the root context for the main function. TODO is a placeholder for code that should receive a real context. {{< /faq >}}

Try It Yourself

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    select {
    case <-time.After(2 * time.Second):
        fmt.Println("completed")
    case <-ctx.Done():
        fmt.Println("cancelled:", ctx.Err())
    }
}

Expected output:

cancelled: context deadline exceeded

What's Next

Now that you understand context, explore worker pools for managing concurrent work.

Topic Description Link
Go Worker Pools Managing concurrent work {{< ref "23-worker-pools" >}}
Go Concurrency Patterns Advanced patterns {{< ref "24-concurrency-patterns" >}}
Go HTTP Servers Building web servers {{< ref "27-http-server" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse Go