Home/Go/Context, cancellation, and channel lifetimes

Context, cancellation, and channel lifetimes

Cancellation propagation, deadlines, backpressure and owned channel closure

Context carries cancellation, deadlines and request-scoped metadata across call boundaries. Pass it explicitly as the first parameter instead of storing it in a long-lived service. context.Background is a root for work with no parent request; context.TODO marks an unresolved propagation decision. A derived context's cancel function releases its resources and should be called even after successful work. Parent cancellation propagates downward, while canceling a child does not cancel its parent.

go
parent := context.Background()
child, cancel := context.WithCancel(parent)
cancel()
fmt.Println(child.Err() == context.Canceled) // true
fmt.Println(parent.Err() == nil) // true

Cancellation is cooperative

Closing Done announces cancellation; it does not kill goroutines, interrupt arbitrary computation, or undo writes already committed. Every potentially blocking stage needs to select on Done or call a dependency that honors context. A CPU loop must check periodically if prompt cancellation matters. Once a context is canceled it cannot be reused as an active context for another request.

WithDeadline and WithTimeout bound work with a deadline. Check errors.Is for context.Canceled or context.DeadlineExceeded where callers need to distinguish user cancellation from an elapsed budget. Avoid deriving fresh unrelated background contexts inside a request, which disconnects downstream work from its owner. When carrying values, use a private key type to avoid collisions and reserve them for request metadata; ordinary required dependencies and options belong in parameters or structs.

Backpressure makes cancellation necessary

An unbuffered output naturally limits producer speed to the consumer's ability to receive. If the consumer leaves, the producer can become stuck in a send unless the send also watches cancellation. Buffering delays the problem but cannot solve an indefinitely absent consumer. A channel bridge must watch cancellation while waiting for input and while delivering output.

The bridge's owned output channel signals that no further values will arrive. A separate result channel carries exactly one terminal error (or nil success) then closes. It is buffered so the bridge can complete cleanup even if the caller has not yet read its status. The caller owns the input; the bridge must not close it. The parent context also belongs to the caller and remains active on normal completion.

Select may choose any ready case, so cancellation races with already-ready input/output. Do not promise an exact partially forwarded prefix under concurrent cancellation. Promise that all values are forwarded in order on normal completion and that blocked boundaries can exit on cancellation. A pre-canceled context can be checked before entering select to give deterministic early behavior. Tests below coordinate transitions with channels and use timeouts only to catch hangs.

Put it into practice

Implement the exercise in a full-screen workspace, then run its tests.

Open practice
Next lessonSynchronization and race-free state