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…