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.
Why Context?
Section titled “Why Context?”The Problems Context Solves
Section titled “The Problems Context Solves”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.
Creating Contexts
Section titled “Creating Contexts”Background and TODO
Section titled “Background and TODO”context.Background() is the root context, typically used in main, init, or tests:
WithCancel
Section titled “WithCancel”WithCancel returns a derived context that can be cancelled:
WithTimeout and WithDeadline
Section titled “WithTimeout and WithDeadline”Set automatic cancellation after a duration or at a specific time:
WithValue
Section titled “WithValue”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 style | Verdict |
|---|---|
ctx.Value("userID") - untyped string | Never. 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 struct | Best. 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.
Context in HTTP Handlers
Section titled “Context in HTTP Handlers”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.
Propagating Context
Section titled “Propagating Context”Always pass context as the first parameter:
Context Best Practices
Section titled “Context Best Practices”Don’ts
Section titled “Don’ts”Cancellation Patterns
Section titled “Cancellation Patterns”Cancel Multiple Goroutines
Section titled “Cancel Multiple Goroutines”First Result Wins
Section titled “First Result Wins”Modern Context (Go 1.20 - 1.23)
Section titled “Modern Context (Go 1.20 - 1.23)”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.
WithCancelCause / context.Cause (Go 1.20)
Section titled “WithCancelCause / context.Cause (Go 1.20)”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.
context.WithoutCancel (Go 1.21)
Section titled “context.WithoutCancel (Go 1.21)”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.
context.AfterFunc (Go 1.21)
Section titled “context.AfterFunc (Go 1.21)”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.
Key Takeaways
Section titled “Key Takeaways”- Always pass context as the first parameter named
ctx - Use
WithCancelfor manual cancellation of goroutines - Use
WithTimeout/WithDeadlinefor automatic time-based cancellation - Use
WithValuesparingly - only for request-scoped data - Always call cancel (usually with
defer cancel()) - Check
ctx.Done()in loops and long operations - Never store context in structs - pass it per-call
- Use custom types for keys to avoid collisions
Exercise
Section titled “Exercise”Create a rate-limited API client that respects context cancellation:
Context-Aware API Client
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.
Practice
Section titled “Practice”Context shows up in almost every exercise that touches concurrency or I/O. Good next steps:
- Rate Limiter - the pattern from the exercise above, built properly
- Worker Pool - cancel a whole pool from one context
- Pipeline Processor - propagate cancellation through pipeline stages
- Retry Patterns - deadlines and back-off working together
- All concurrency exercises - or browse the full exercise battery
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/cmppackages already have what you need - Context (Chapter 14) - cancellation, deadlines and request-scoped values, including the
Cause/WithoutCancel/AfterFuncadditions 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.
Where to go next
Section titled “Where to go next”- 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, andcontextitself.
Go forth and build great things.