The context package is Go's standard mechanism for carrying deadlines, cancellation signals, and request-scoped values across API boundaries and between goroutines. Every function that performs I/O, launches goroutines, or calls into another service should accept a context.Context as its first parameter. This is not a convention — it is enforced by the standard library itself, from net/http to database/sql to os/exec.
Contexts form a tree. context.Background() is the root, typically created once at program startup or at the top of an HTTP handler. From there, you derive child contexts with WithCancel, WithTimeout, WithDeadline, or WithValue. When a parent context is cancelled, all its children are cancelled automatically. This propagation is the key insight: a single cancel call at the HTTP handler level tears down the entire goroutine tree spawned by that request — database queries, outbound HTTP calls, background computations — without any goroutine needing to know about the others.
WithValue attaches request-scoped metadata (request IDs, authentication tokens, trace spans) to the context. It is deliberately awkward to use — the API requires any keys and values with no type safety — because it is meant for cross-cutting concerns that transit process boundaries, not for passing function parameters. Overusing WithValue creates implicit dependencies that are invisible in function signatures and impossible to trace at compile time.
Master go context package
Take the Go Programming course with hands-on lessons and challenges.
Three independent pollers share a context derived with WithCancel. Each goroutine selects on ctx.Done() alongside its ticker. A single cancel() call tears down all three goroutines simultaneously. ctx.Err() returns context.Canceled so each goroutine knows why it stopped.
WithTimeout creates a context that automatically cancels after 500ms. The slowQuery function selects between completing its work and the context deadline. When the timeout fires, ctx.Done() closes and the function returns a wrapped context.DeadlineExceeded error. The deferred cancel() is still required to release the internal timer even if the function completes before the timeout.
The HTTP request context (r.Context()) is automatically cancelled when the client disconnects. The handler derives a child context with a 500ms timeout to bound total processing time. Both the database and API calls receive this context and abort early if the deadline passes or the client disconnects. This uses Go 1.22 enhanced ServeMux routing with path parameters.
WithValue attaches the request ID to the context so any function in the call chain can access it without threading it through every parameter list. The unexported ctxKey type ensures no other package can accidentally overwrite the value. Helper functions WithRequestID and RequestID provide a type-safe API over the untyped context.Value mechanism.
Every pipeline stage wraps its channel send in a select with ctx.Done(). When the consumer only needs 3 values, calling cancel() unblocks all stages simultaneously. Without context, the generate goroutine would block forever trying to send values that nobody reads. This is the pattern recommended by the Go blog for building cancellation-safe pipelines.
Contexts nest naturally. The outer context imposes a 2-second request deadline. Within processRequest, the database query gets a stricter 500ms sub-deadline. If the DB query times out, it does not cancel the parent context — only the dbCtx child. But if the parent is cancelled (client disconnect), all children cancel too. This layered approach lets you enforce both per-step and overall request budgets.
Forgetting to call the cancel function returned by WithCancel/WithTimeout/WithDeadline — this leaks the internal goroutine that the context package creates to manage the timer
Always `defer cancel()` immediately after creating a derived context. Even if the context expires naturally, calling cancel() is required to free resources. The cancel function is idempotent — calling it multiple times is safe.
Storing contexts in structs instead of passing them as function parameters — this makes cancellation scope ambiguous and breaks the context tree model
Pass context.Context as the first parameter named ctx to every function that needs it. The Go standard library follows this convention universally. A context stored in a struct becomes stale and does not reflect the caller's lifecycle.
Using context.WithValue to pass required function arguments (database connections, loggers, configuration) instead of explicit parameters
WithValue is for cross-cutting request-scoped metadata (request IDs, trace spans, auth tokens) that transits API boundaries. Required dependencies should be explicit function parameters or struct fields. If you find yourself doing ctx.Value() in business logic, refactor to pass the value directly.
Using context.Background() deep inside a call chain instead of propagating the caller's context — this breaks cancellation propagation so goroutines ignore client disconnects and timeouts
Thread the context from the outermost handler through every function call. context.Background() should only appear at the program entry point (main, init) or in tests. If a library function does not accept a context, wrap it with a select on ctx.Done().
The context package provides Go's standard mechanism for propagating deadlines, cancellation signals, and request-scoped values through a call graph. Derived contexts form a tree where cancelling a parent cancels all descendants. WithTimeout bounds operation duration, WithCancel enables manual shutdown, and WithValue carries metadata across API boundaries. Every function that does I/O or spawns goroutines should accept a context.Context as its first parameter and check ctx.Done() in select statements to enable cooperative cancellation.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.