context.Context

Course 3 · Ch 3
context.Context: Cancellation, Timeouts, and Request-Scoped Values
One value, passed through every layer of a call chain, that can say "stop now" to everything downstream at once

Intermediate Chapter 3 showed goroutines running independently, with no automatic way for main() to know when they're done short of a channel or WaitGroup. context.Context solves a related but different problem: how does code running several layers deep — goroutines, function calls, network requests — find out that it should stop, because a timeout passed or the caller gave up? There's no real JavaScript equivalent to reach for here; the closest conceptual cousin is an AbortController, but Go's version is used far more pervasively.

The Basic Pattern — Pass ctx as the First Parameter

import "context" func doWork(ctx context.Context) { select { case <-ctx.Done(): fmt.Println("Work cancelled:", ctx.Err()) case <-time.After(2 * time.Second): fmt.Println("Work finished normally") } }

By strong convention, ctx context.Context is always the first parameter of any function that supports cancellation — never buried among other arguments. ctx.Done() returns a channel (Intermediate Chapter 3) that something closes the moment cancellation happens; select here waits on whichever of two channels is ready first, a new construct specifically for working with multiple channels at once.

context.WithTimeout — Cancel Automatically After a Duration

ctx, cancel := context.WithTimeout(context.Background(), 1 * time.Second) defer cancel() // always defer cancel — releases resources even if it times out first doWork(ctx) // Work cancelled: context deadline exceeded

context.Background() creates a brand-new, empty starting context — the root every other context derives from. context.WithTimeout(parent, duration) wraps it with a deadline: after that duration, ctx.Done() closes automatically, and ctx.Err() reports context deadline exceeded. doWork here finishes via the timeout branch instead of its normal 2-second wait, since the 1-second deadline fires first.

Always defer cancel(), even on success
WithTimeout (and its cousin WithCancel) both return a cancel function that must be called to release the context's internal resources, whether or not the timeout ever actually triggers. defer cancel() immediately after creating the context is the standard, safe habit — skipping it can leak resources in long-running programs.

context.WithCancel — Manual Cancellation

ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(500 * time.Millisecond) cancel() // some other goroutine decides it's time to stop }() doWork(ctx) // Work cancelled: context canceled

WithCancel hands back a cancel function with no automatic timer attached — cancellation happens only when something explicitly calls it, from anywhere that has a reference to that function. This is the pattern used when one part of a program needs to tell every dependent goroutine to stop, on demand, rather than after a fixed duration.

Passing Request-Scoped Values

type contextKey string const userIDKey contextKey = "userID" ctx := context.WithValue(context.Background(), userIDKey, 42) func handleRequest(ctx context.Context) { userID := ctx.Value(userIDKey) fmt.Println("Handling request for user", userID) }

context.WithValue attaches a single key/value pair to a context, retrievable anywhere downstream via ctx.Value(key) — most commonly used for request-scoped data in a web server (a user ID, a trace ID) that many unrelated layers of code might need without explicitly threading it through every function signature.

Don't use context.WithValue for ordinary function parameters
It's tempting to reach for WithValue as a way to avoid adding parameters to a function — resist this. It's intended specifically for cross-cutting, request-scoped data (tracing IDs, auth info), not as a general substitute for normal arguments, which stay clearer and more type-safe as actual parameters.
ToolPurpose
context.Background()The root context everything else derives from
context.WithTimeout(parent, d)Auto-cancels after duration d; always defer cancel()
context.WithCancel(parent)Manual cancellation via the returned cancel function
context.WithValue(parent, key, val)Attaches request-scoped data, retrieved with ctx.Value(key)
ctx.Done() / ctx.Err()Channel that closes on cancellation; reports why

Coding Challenges

Challenge 1

Write a function countTo(ctx context.Context, n int) that counts from 1 to n, printing each number with a 300ms pause between them, but stops early and prints "Stopped early" if ctx is cancelled. Call it with a context.WithTimeout of 1 second and n = 10, so it stops before reaching 10.

📄 View solution
Challenge 2

Using context.WithCancel, start a goroutine that prints "Working..." every 200ms until cancelled. In main, let it run for about 1 second, then call cancel() and confirm (via a short Sleep and a printed message) that the goroutine stopped.

📄 View solution
Challenge 3

Define a contextKey type and a requestIDKey constant. Use context.WithValue to attach a request ID string to a context, then write a function logRequest(ctx context.Context) that reads it back with ctx.Value and prints it.

📄 View solution

Chapter 3 Quick Reference

  • ctx context.Context — always the first parameter of a cancellation-aware function
  • context.Background() — the root context to start from
  • context.WithTimeout(parent, duration) — auto-cancels after a fixed time; always defer cancel()
  • context.WithCancel(parent) — manual cancellation via the returned function
  • context.WithValue(parent, key, value) — attaches request-scoped data
  • ctx.Done() — a channel that closes when the context is cancelled or times out
  • ctx.Err() — explains why: context.Canceled or context.DeadlineExceeded
  • Next chapter: testing — the testing package, table-driven tests, and go test