Skip to content

Chapter 14: Context Package

The context package is essential for managing cancellation, deadlines, and request-scoped values across API boundaries and goroutines. It’s the standard way to control the lifetime of operations in Go.

Context is Go’s answer to a fundamental question: how do you propagate cancellation and deadlines through a call graph? When a user cancels a request, how does that signal reach every goroutine working on that request? Context provides a standard, composable solution.

Introduced in Go 1.7 and now ubiquitous in Go APIs, context forms a tree structure where parent cancellation automatically propagates to children. This cascading cancellation prevents resource leaks and ensures graceful shutdowns.

This chapter covers context creation, cancellation patterns, timeouts, deadlines, and request-scoped values. You’ll learn when to use context, how to propagate it correctly, and common pitfalls to avoid.

When handling requests (HTTP, gRPC, database queries), you often need to:

  • Cancel work when a request is cancelled: User closes browser, don’t waste resources
  • Set timeouts for operations: Prevent operations from running forever
  • Pass request-scoped data: User ID, trace ID, authentication tokens across the call stack

Without context, these are hard problems. Cancellation requires passing done channels everywhere. Timeouts need manual timer management. Request data either goes in globals (bad) or requires threading through every function signature (tedious).

Context solves all three elegantly. It’s passed as the first parameter to functions, carrying cancellation signals, deadlines, and values. The pattern is universal across the Go ecosystem.

context.Background() is the root context, typically used in main, init, or tests:

WithCancel returns a derived context that can be cancelled:

Set automatic cancellation after a duration or at a specific time:

Pass request-scoped values through the context.

Choosing a key type matters more than anything else about WithValue. ctx.Value looks keys up by interface equality, walking the whole parent chain, so any package that happens to use an equal key will read - or shadow - your value. There are three levels of safety:

Key styleVerdict
ctx.Value("userID") - untyped stringNever. Any package using the same literal collides with you silently. go vet flags this.
type ctxKey string; const userIDKey ctxKey = "userID"Acceptable. A defined type in your package cannot equal another package’s key.
type userIDKey struct{} - unexported empty structBest. Zero-sized, impossible to construct from outside your package, and self-documenting.

Pair the key with exported setter/getter functions and keep the key type unexported. Callers then get a type-safe API and never touch ctx.Value at all:

The Do’s and Don’ts sections below use exactly this pattern - the empty-struct key with an exported accessor. That is the one to copy.

Every HTTP request comes with a context.

Note how the handler tests the error: errors.Is(err, context.DeadlineExceeded), never err == context.DeadlineExceeded. By the time an error has travelled up through a few layers it has usually been wrapped with fmt.Errorf("...: %w", err), and a == comparison against the sentinel then silently returns false - so the handler falls through to a 500 instead of the 504 it should return. errors.Is unwraps, so it keeps working no matter how many layers wrap the error.

Always pass context as the first parameter:

The four context functions everyone learns - Background, WithCancel, WithTimeout, WithValue - have been stable since Go 1.7. But the package has grown four genuinely useful additions since Go 1.20, and most tutorials (and most AI-generated Go) predate all of them.

The oldest wart in the package: ctx.Err() only ever returns context.Canceled or context.DeadlineExceeded. When something deep in your call graph cancels a shared context, every other goroutine learns that it was cancelled but never why. Teams worked around this with a side-channel error field guarded by a mutex.

context.WithCancelCause returns a cancel func(error) instead of cancel func(). Whatever error you pass is retrievable with context.Cause(ctx), and it works with errors.Is/errors.As. ctx.Err() still returns context.Canceled, so existing code is unaffected.

WithTimeoutCause / WithDeadlineCause (Go 1.21)

Section titled “WithTimeoutCause / WithDeadlineCause (Go 1.21)”

The same idea for time-based cancellation: attach an error explaining which budget expired. Invaluable when a request passes through three services that each impose a deadline - context deadline exceeded alone never tells you whose deadline it was.

Sometimes work must outlive the request that started it: flushing metrics, writing an audit record, finishing a span. Passing the request context means the work is killed the instant the client disconnects; passing context.Background() throws away the trace ID, auth info, and every other value. WithoutCancel gives you the third option - keep the values, drop the cancellation and the deadline.

Registers a function to run in its own goroutine when a context is done. It replaces the boilerplate go func() { <-ctx.Done(); cleanup() }(), and crucially it returns a stop() that unregisters the callback if it hasn’t fired - so you don’t leak a goroutine blocked on Done() for a context that is never cancelled.

  1. Always pass context as the first parameter named ctx
  2. Use WithCancel for manual cancellation of goroutines
  3. Use WithTimeout/WithDeadline for automatic time-based cancellation
  4. Use WithValue sparingly - only for request-scoped data
  5. Always call cancel (usually with defer cancel())
  6. Check ctx.Done() in loops and long operations
  7. Never store context in structs - pass it per-call
  8. Use custom types for keys to avoid collisions

Create a rate-limited API client that respects context cancellation:

Context-Aware API Client

medium

Implement a simple API client that fetches data with rate limiting and respects context cancellation/timeouts. At 2 requests/second under a 2-second deadline, the first four endpoints succeed and the fifth is cancelled - that last error is the point of the exercise, not a failure.

Context shows up in almost every exercise that touches concurrency or I/O. Good next steps:


Congratulations - you’ve finished the book

Section titled “Congratulations - you’ve finished the book”

That’s all fourteen chapters. Working forward from interfaces and error handling, through goroutines and sync primitives, into architecture, testing and profiling, and finally to the two features that define modern Go:

  • Generics (Chapter 13) - type parameters, constraints, and knowing when the slices/maps/cmp packages already have what you need
  • Context (Chapter 14) - cancellation, deadlines and request-scoped values, including the Cause/WithoutCancel/AfterFunc additions most tutorials still omit

Two things worth carrying with you. First, measure instead of guessing - Chapter 11 exists because intuition about Go performance is wrong more often than it’s right. Second, check the standard library before you write anything clever - slices, maps, cmp, errors, and context have absorbed a lot of what used to be hand-rolled.

  • The exercise battery - concurrency, testing, architecture, error handling and generics tracks, each with graded exercises and worked solutions
  • Quick Reference - the whole book condensed for when you just need the syntax
  • Read real Go: the standard library is the best-commented Go you will ever find. Start with net/http, sync, and context itself.

Go forth and build great things.


Chapter in progress
0 / 14 chapters completed

Back to Quick Reference