Lesson 14 / 25

Propagating Cancellation

Passing ctx down and honouring it in goroutines.

Pass it down, check it in loops

Cancellation is cooperative: a context cannot stop a goroutine by force. Every function that does I/O or may block should accept a ctx and pass it to whatever it calls, for example http.NewRequestWithContext(ctx, ...) or db.QueryContext(ctx, ...), so that a single cancel at the top stops the whole tree of work. A goroutine that loops must check ctx.Done() in its select, both when waiting for input and when sending output, because a send to a consumer that has gone away would otherwise block forever. Long CPU-bound loops can check ctx.Err() != nil every so many iterations. In HTTP servers, r.Context() is cancelled when the client disconnects or the handler returns, so derive from it rather than from Background.

A producer that respects cancellation on send

Both the wait and the send are guarded by ctx.Done().

package main

import (
	"context"
	"fmt"
)

func numbers(ctx context.Context) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for i := 0; ; i++ {
			select {
			case out <- i:
			case <-ctx.Done():
				return // consumer is gone: exit instead of blocking forever
			}
		}
	}()
	return out
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	for n := range numbers(ctx) {
		if n == 3 {
			break
		}
		fmt.Println(n)
	}
	cancel() // stops the producer goroutine; defer would also do it
}

Do not replace a request context with Background

Calling context.Background() deep inside a request handler cuts the cancellation chain: the work keeps running after the client has gone. If work must outlive the request, make that explicit (for example with context.WithoutCancel in Go 1.21+) and give it its own timeout.

Quick check: Why must a producer goroutine also select on ctx.Done() when sending?

  • Channels are closed automatically on cancellation
  • Sending is not allowed without a context
  • ctx.Done() makes the send faster
  • If the consumer stops receiving, the send would block forever and leak the goroutine
Answer

If the consumer stops receiving, the send would block forever and leak the goroutine — Cancellation is cooperative; every blocking point must be able to observe it.