Before this:Channels
Context & cancellation
Key takeaways
A context.Context carries a cancellation signal and deadline down a call tree.
WithCancel and WithTimeout derive a child context plus a cancel
function; calling cancel (or hitting the timeout) closes ctx.Done(), a
channel every goroutine can select on to stop cleanly. By convention ctx is
the first argument of any cancellable function.
Long-running programs need a way to say “stop now” — on shutdown, a timeout, or a
user’s Ctrl-C. This lesson covers Go’s answer, context, which builds directly on
channels and
select.
Creating a cancellable context
You start from a root (context.Background()) and derive children that can be
cancelled:
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // always call cancel to release resources
go scan(ctx) // pass ctx down to the work
// ...later, to stop everything:
cancel()
WithTimeout and WithDeadline do the same but also cancel automatically after a
duration:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
Reacting to cancellation
The heart of it is ctx.Done() — a channel that closes when the context is
cancelled. A worker selects on it alongside its normal work:
func scan(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err() // Canceled or DeadlineExceeded
case sample := <-samples:
process(sample)
}
}
}
The moment cancel() runs (or the timeout fires), the <-ctx.Done() case wins and
the goroutine returns. One signal, cleanly propagated.
One signal, a whole tree
Because each derived context is a child of its parent, cancelling the parent cancels every descendant. A single Ctrl-C at the top can shut down every goroutine in a GopherTrunk pipeline — the SDR reader, the demodulators, the decoder — without any of them knowing about each other.
Rules of the road
| Do | Don’t |
|---|---|
Pass ctx as the first argument |
Store a Context inside a struct |
Always call the returned cancel (defer it) |
Pass a nil context |
Return ctx.Err() when Done fires |
Ignore cancellation in a long loop |
Use WithTimeout for network/I/O deadlines |
Use context to pass optional arguments |
Follow these and cancellation becomes a habit rather than an afterthought — essential for the concurrent shapes in the next lesson.
Quick check: how does a goroutine learn its context was cancelled?
Recap
- A
Contextcarries a cancel signal and deadline down a call tree. WithCancel/WithTimeoutreturn a child ctx and acancel— always call cancel.ctx.Done()is a channel youselecton; returnctx.Err()when it closes.- Pass
ctxfirst; cancelling a parent cancels every child.
Next up: the reusable shapes of concurrent code — worker pools, pipelines, and fan-out/fan-in.
Frequently asked questions
Why is ctx the first argument to so many Go functions?
By convention a Context is passed as the first parameter, named ctx, so a cancel signal or deadline flows explicitly down the whole call tree. Making it the first argument means every function that might block or do I/O can be told to stop, and readers can see at a glance that a call is cancellable.
What does ctx.Done() actually give me?
It returns a channel that is closed when the context is cancelled or its deadline passes. You select on it: when the receive succeeds (because the channel closed), it’s time to stop work and return ctx.Err(). It’s the standard way a goroutine learns it should shut down.