iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

context

context.Context carries cancellation signals, deadlines, and request-scoped values across API boundaries. Every server, worker, and goroutine that does I/O should accept a context as its first parameter — the standard way to wire shutdown, timeouts, and tracing through Go programs.

WithCancel, deadline, values, propagation

EXAMPLE
package main

import (
    "context"
    "errors"
    "fmt"
    "net/http"
    "time"
)

// 1) Background + TODO — root contexts
ctx := context.Background()              // top-level, e.g. main()
ctx := context.TODO()                     // placeholder when you'll wire a real one later

// 2) WithCancel — manual cancellation
ctx, cancel := context.WithCancel(context.Background())
defer cancel()                            // ALWAYS defer cancel — even on success

go func() {
    time.Sleep(2 * time.Second)
    cancel()                              // signal cancellation
}()

<-ctx.Done()                              // blocks until cancelled
fmt.Println("cancelled:", ctx.Err())      // context.Canceled

// 3) WithTimeout — auto-cancel after duration
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()

if err := slowJob(ctx); err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        fmt.Println("timed out")
    }
}

// 4) WithDeadline — absolute time
deadline := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

// 5) WithValue — request-scoped data (use sparingly)
type userIDKey struct{}                  // unexported type avoids collisions
ctx := context.WithValue(context.Background(), userIDKey{}, "u_42")

func handler(ctx context.Context) {
    if uid, ok := ctx.Value(userIDKey{}).(string); ok {
        fmt.Println("user:", uid)
    }
}

// Don't smuggle business logic args through context. Use it for:
//   • Request ID / trace ID / span
//   • Authenticated user (when middleware sets it)
//   • Tenant ID in multi-tenant systems

// 6) HTTP client with context
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/me", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
    if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
        // handle
    }
}

// 7) HTTP server — request context
func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()                    // cancelled when client disconnects
    if err := db.QueryRowContext(ctx, "SELECT 1").Scan(&v); err != nil {
        // ctx.Err() != nil if client gave up
    }
}

// http.Server cancels r.Context() when:
//   • Client closes the connection
//   • Server.Shutdown() is called (with grace period)

// 8) Worker / goroutine pattern — check ctx.Done
func worker(ctx context.Context, jobs <-chan Job) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case j, ok := <-jobs:
            if !ok { return nil }
            if err := j.Run(ctx); err != nil { return err }
        }
    }
}

// 9) Propagating to multiple downstream calls
func enrich(ctx context.Context, id string) (*User, error) {
    // Both calls share the same deadline + cancellation
    profile, err := fetchProfile(ctx, id)
    if err != nil { return nil, err }
    addr, err := fetchAddress(ctx, id)
    if err != nil { return nil, err }
    return &User{Profile: profile, Address: addr}, nil
}

// 10) errgroup — context + fan-out + error propagation
import "golang.org/x/sync/errgroup"

func fetchAll(ctx context.Context, ids []string) ([]*User, error) {
    users := make([]*User, len(ids))
    g, gctx := errgroup.WithContext(ctx)
    g.SetLimit(8)                          // max 8 concurrent
    for i, id := range ids {
        i, id := i, id
        g.Go(func() error {
            u, err := fetchUser(gctx, id)
            if err != nil { return err }
            users[i] = u
            return nil
        })
    }
    if err := g.Wait(); err != nil { return nil, err }
    return users, nil
}

// 11) Graceful shutdown
func main() {
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()

    srv := &http.Server{Addr: ":8080", Handler: mux}
    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    <-ctx.Done()                            // SIGINT / SIGTERM
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    srv.Shutdown(shutdownCtx)
}

// 12) DB drivers + context
// database/sql: db.QueryContext, db.ExecContext, db.PingContext, tx.RollbackContext
// pgx:           conn.Query(ctx, ...)
// mongo-driver:  collection.Find(ctx, filter)
// Always pass the request's ctx; cancellation propagates to the network roundtrip.

// 13) Best practices
// • First parameter, named ctx: func Do(ctx context.Context, …)
// • NEVER store a context in a struct — pass through call chains
// • Always defer cancel() after WithCancel/WithTimeout/WithDeadline
// • Don't pass nil context — use context.TODO()
// • Avoid context.WithValue for app args — for cross-cutting only

// 14) Common bugs
// • Forgot cancel() → goroutine + timer leak (vet warns)
// • ctx.Err() ignored — return early on cancellation to free downstream resources
// • Passing TODO when you have a real ctx — propagate instead
// • Sleeping with time.Sleep — doesn't honour cancellation; use select on time.After + ctx.Done
// • Mixing context.Background in a request handler — bypasses client cancellation
// • Comparing errors with == ctx.Err() — use errors.Is(err, context.DeadlineExceeded)
// • Long-lived contexts in a worker — each unit of work should have a child context
// • Context value shadowed by middleware key collision — use unexported key types

Why it matters

context.Context is the contract for cancellation, deadlines, and per-request values across goroutines and API boundaries. Always pass ctx as the first argument, defer cancel(), and use errgroup.WithContext for fan-out work so the first error cancels the rest.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
Try it Yourself »

Discussion

Loading…