Patterns: concurrent Go · 3 MIN READ

Cancellation: propagate an exit path through every handoff

Make a producer stop when its consumer no longer needs results.

Make a producer stop when its consumer no longer needs results. Cancellation is a cooperative signal, not a forced goroutine kill. Trace the worked Go example, then compare the benefit with the cost before applying this design to your own project.

Concurrent Go pattern · A MAP OF THE IDEA

  1. Caller owns and cancels the context
  2. Blocking handoff selects on Done
  3. Producer closes output and the caller waits

Useful when

  • A consumer can stop before a stream ends
  • Work needs deadlines or shutdown coordination

Watch out

  • Cancellation is cooperative and can race with delivery
  • Unsupported blocking APIs need separate lifecycle design

Implementation & operation cost: Medium

Fit depends on the task

Small tool or CLI5/5
Domain-rich application5/5
Independent distributed services5/5

Editorial suitability, not benchmarks or a universal ranking. Styles describe different axes and can be combined.

01

The design decision

Cancellation is a cooperative signal, not a forced goroutine kill. Blocking channel operations must observe it, and the caller still needs a way to wait for completion. Carry context through the operation instead of storing request contexts in long-lived structs.

02

A complete Go example

Read this program from the entry point and follow the calls shown in the diagram. The output is included at the end so you can check the behavior without a compiler.

go
package main

import (
	"context"
	"fmt"
)

func Numbers(ctx context.Context) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for i := 1; ; i++ {
			select {
			case out <- i:
			case <-ctx.Done():
				return
			}
		}
	}()
	return out
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	out := Numbers(ctx)
	first := <-out
	cancel()
	for range out {
	} // Wait until the producer closes its channel.
	fmt.Println(first, ctx.Err())
}

// Output:
// 1 context canceled
Read the code

The producer can leave a blocked send through ctx.Done and closes its own output on return. main cancels after the first value, then drains until completion. A racing send may already be ready when cancellation arrives, so cancellation does not promise zero additional values.

03

Tradeoffs and the simpler alternative

Cancel on all completion paths to release timers and derived contexts. Do not replace completion waiting with time.Sleep. Real I/O adapters must support deadlines or cancellation themselves; passing a context does not interrupt an arbitrary blocking function.

TAKE THIS WITH YOU

Requires blocking work to observe the cancellation signal. Cancel on all completion paths to release timers and derived contexts. Do not replace completion waiting with time.Sleep. Real I/O adapters must support deadlines or cancellation themselves; passing a context does not interrupt an arbitrary blocking function.

Check your understanding

Which statement best describes Cancellation: propagate an exit path through every handoff?

Go to the source

Progress stays in this browser. No account needed.