Skip to content

Day 075 — context Cancellation & Propagation

Month 3 · Week 3 · ⬅ Day 074 · Day 076 ➡ · Journal index

🎯 Learning Objective

Use context.Context to carry a cancellation signal and deadline down a call tree, so cancelling a parent stops all derived work, and learn the conventions that keep it leak-free.

📚 Topics

  • WithCancel, WithTimeout, WithDeadline; Done(), Err()
  • Propagation: parent cancel → every child context cancels
  • Conventions: ctx is the first param; always defer cancel()

📖 Reading / Sources

📝 Notes

  • A Context carries a cancellation signal, an optional deadline, and request-scoped values across API boundaries and goroutines → [[context]].
  • Derive children with context.WithCancel/WithTimeout/WithDeadline(parent). Each returns a cancel function — always defer cancel(), even on the timeout variants, to release the context's resources (a timer, a goroutine) immediately → [[context-cancellation]].
  • Propagation is one-way and downward: cancelling a parent cancels every context derived from it; cancelling a child does not affect the parent. Building a tree means one cancel() can tear down a whole subtree.
  • ctx.Done() returns a channel closed on cancellation; select on it. ctx.Err() then explains why: context.Canceled (explicit cancel) or context.DeadlineExceeded (timeout/deadline) → [[ctx-err]].
  • Conventions (enforced by go vet/lint): pass ctx as the first parameter, named ctx; never store a Context in a struct field for later; never pass a nil Context — use context.TODO() if you truly don't have one yet → [[context-first-param]].
  • WithTimeout(parent, d) is just WithDeadline(parent, time.Now().Add(d)). The context cancels itself when the deadline passes — no goroutine needed to fire it.
  • context.Value is for request-scoped data (trace IDs, auth) crossing API boundaries — not for passing optional function parameters. Overuse makes data flow invisible → [[context-values]].
  • Cancellation is cooperative: a context can't forcibly stop a goroutine. The goroutine must check ctx.Done() (or pass ctx to a blocking call that does). Ignored contexts = leaks → [[goroutine-leak]].

💻 Code Examples

// Race the work against the context: a cancelled/expired ctx returns its error
// immediately instead of blocking for the full duration.
func slowWork(ctx context.Context, d time.Duration) error {
    select {
    case <-time.After(d):
        return nil
    case <-ctx.Done():
        return ctx.Err() // context.Canceled or context.DeadlineExceeded
    }
}

ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel() // release the timer even if slowWork returns early
err := slowWork(ctx, 2*time.Second)
fmt.Println(errors.Is(err, context.DeadlineExceeded)) // true

Full code: examples/month-03/context-cancel/main.go · Run: go run ./examples/month-03/context-cancel

🏋️ Exercises / Practice

Exercise Status Link
Semaphore.AcquireCtx (cancellable acquire) exercises/month-03/week-3/semaphore/
Bounded Map returning ctx.Err() on cancel exercises/month-03/week-3/boundedmap/

🐛 Mistakes Made

  • Created a WithTimeout context and forgot defer cancel()go vet flagged "the cancel function is not used"; left a timer running until the deadline. Added the defer.
  • Compared err == context.DeadlineExceeded directly; fine here, but switched to errors.Is so wrapped errors still match → [[error-wrapping]].

❓ Open Questions

  • How do I cancel a group of goroutines and collect the first error in one shot? (That's errgroup — Day 076.)

🧠 Active Recall (answer without looking)

  1. Q: What's the difference between context.Canceled and context.DeadlineExceeded, and how do you read which occurred?

    A `context.Canceled` means someone called the `cancel` func; `context.DeadlineExceeded` means the deadline/timeout passed. After `<-ctx.Done()`, call `ctx.Err()` (compare with `errors.Is`) to see which.

  2. Q: Why must you call cancel() even when you used WithTimeout?

    A The context holds resources (a timer and an internal goroutine) until either the deadline fires or `cancel` is called. `defer cancel()` releases them promptly instead of leaking until the deadline.

🪶 Feynman Reflection

A context is a "stop work" walkie-talkie handed down a chain of command. The boss can press cancel (or set a timer that presses it); every subordinate holding the same channel hears the click via ctx.Done() and stops. But it's cooperative — a worker wearing headphones (never checking Done()) keeps going, which is exactly how goroutine leaks happen.

🕳️ Knowledge Gaps

  • Wiring context through real network calls (HTTP/gRPC) with deadlines — comes up in the API month.

✅ Summary

I can create and propagate contexts, distinguish Canceled vs DeadlineExceeded, follow the ctx-first/defer cancel() conventions, and remember cancellation is cooperative — workers must check Done().

⏭️ Next Steps / Prep for Tomorrow

  • Day 076: errgroup for grouped cancellation + first-error, and rate limiting.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: feat(examples): context cancellation, deadlines & propagation (day 075)