Lesson 17 of 30 advanced 5 min read

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.

root ctx — cancel() reader demod decoder
Cancelling the root context closes Done() for every child, so one signal stops the whole tree.

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 Context carries a cancel signal and deadline down a call tree.
  • WithCancel/WithTimeout return a child ctx and a cancel — always call cancel.
  • ctx.Done() is a channel you select on; return ctx.Err() when it closes.
  • Pass ctx first; 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.