Skip to content
Codeloom
Go

The context Package in Go

Master Go's context package — cancellation, timeouts, deadlines, values, and how to propagate them through your application stack.

·4 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • What context.Context is and why every Go API uses it
  • Cancellation propagation with WithCancel
  • Timeouts and deadlines with WithTimeout and WithDeadline
  • Best practices for context usage

Prerequisites

  • Go basics (goroutines, channels)
  • Understanding of concurrent request handling

The context package solves a fundamental problem: how do you tell a tree of goroutines to stop? Every Go server handler, database call, and HTTP client accepts a context.Context as its first parameter. Here is how to use it.

The Context interface

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key any) any
}
  • Done() returns a channel that closes when the context is cancelled.
  • Err() returns why: context.Canceled or context.DeadlineExceeded.
  • Deadline() returns the time when the context will auto-cancel.
  • Value() carries request-scoped data (use sparingly).

Creating contexts

Background and TODO

ctx := context.Background() // root context, never cancelled
ctx := context.TODO()       // placeholder when you haven't decided yet

Use Background() in main(), initialization, and tests. Use TODO() when a function needs a context but you haven’t wired it through yet.

WithCancel

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

go func() {
    select {
    case <-ctx.Done():
        fmt.Println("Worker stopped:", ctx.Err())
        return
    case <-time.After(5 * time.Second):
        fmt.Println("Work done")
    }
}()

time.Sleep(2 * time.Second)
cancel() // signals all goroutines listening on ctx.Done()

WithTimeout

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
    // could be context.DeadlineExceeded
    fmt.Println("Request failed:", err)
    return
}
defer resp.Body.Close()

WithDeadline

Like WithTimeout but you specify an absolute time.

deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

Propagation through a call stack

Context flows down. Every function that might block should accept ctx context.Context as its first parameter.

func handleRequest(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    user, err := fetchUser(ctx, r.URL.Query().Get("id"))
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    json.NewEncoder(w).Encode(user)
}

func fetchUser(ctx context.Context, id string) (*User, error) {
    row := db.QueryRowContext(ctx, "SELECT name, email FROM users WHERE id = $1", id)
    var u User
    if err := row.Scan(&u.Name, &u.Email); err != nil {
        return nil, fmt.Errorf("querying user: %w", err)
    }
    return &u, nil
}

If the client disconnects, r.Context() cancels. The database query stops. No wasted work.

Context values

Carry request-scoped data like request IDs, auth tokens, or trace spans.

type contextKey string

const requestIDKey contextKey = "requestID"

func withRequestID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, requestIDKey, id)
}

func getRequestID(ctx context.Context) string {
    if id, ok := ctx.Value(requestIDKey).(string); ok {
        return id
    }
    return "unknown"
}

Use unexported key types to avoid collisions across packages.

Listening for cancellation in goroutines

func worker(ctx context.Context, jobs <-chan Job) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Worker shutting down")
            return
        case job := <-jobs:
            process(job)
        }
    }
}

Best practices

  1. Always pass context as the first parameter. Convention: func Foo(ctx context.Context, ...).
  2. Always call cancel. Use defer cancel() immediately after creating a derived context.
  3. Do not store contexts in structs. Pass them explicitly through function calls.
  4. Use values sparingly. Only for request-scoped data that crosses API boundaries (trace IDs, auth tokens). Never for function parameters.
  5. Check ctx.Err() before starting expensive work.
func expensiveOperation(ctx context.Context) error {
    if ctx.Err() != nil {
        return ctx.Err()
    }
    // proceed with expensive work...
    return nil
}

Summary

The context package is Go’s standard mechanism for cancellation, timeouts, and request-scoped values. Pass it as the first parameter, always defer the cancel function, and check ctx.Done() in long-running goroutines. It is the connective tissue that lets your entire call stack respond to shutdown signals.