Course outline

Go / Intermediate: Packages and Concurrent Work / Lesson 10

Context, Deadlines, and Cancellation

Propagate operation lifetime across calls and distinguish requesting cancellation from waiting for work to stop.

About 5 min readContent reviewed
On this page

Lesson goals

  • Pass Context explicitly through request-scoped operations.
  • Call cancellation functions and inspect cancellation reasons.
  • Design blocking work that can observe shutdown.

A context carries an operation’s lifetime

context.Context carries cancellation, deadlines, and request-scoped values across API boundaries. Conventionally it is the first parameter, named ctx, and is passed explicitly rather than stored as a permanent struct field. Do not pass nil; choose an appropriate parent. A child derives a narrower lifetime and cannot extend a deadline already imposed by its parent.

A context is safe for simultaneous use by multiple goroutines, but values stored inside it do not automatically become safe mutable shared state. Keep ordinary business arguments as ordinary parameters. Use context values only for request-scoped information crossing API boundaries, with unexported key types that avoid collisions.

Request stop, then wait for completion

WithCancel returns a child and a cancellation function. Calling cancel closes the child’s Done channel eventually and releases associated resources; repeated calls are safe. Cancel does not wait for goroutines to finish, so a separate completion channel or WaitGroup is needed before releasing state the worker may still access.

The first program starts one worker waiting for cancellation, cancels, and then waits for its done signal. The signal is closed by the worker on return. No sleep or scheduler timing is involved. A real worker must place cancellation checks around each blocking stage rather than only checking once at startup.

go Cancellation plus explicit completion
package main

import (
	"context"
	"fmt"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	done := make(chan struct{})
	go func() { defer close(done); <-ctx.Done() }()
	cancel()
	<-done
	fmt.Println("worker stopped", ctx.Err() == context.Canceled)
}
Expected output
worker stopped true

main observes the worker’s completion instead of guessing how long stopping takes.

A deadline is a bound, not a sleeping instruction

WithDeadline and WithTimeout derive cancellation from time. Always call the returned cancel function when the operation ends, even if the deadline might expire later. This frees resources sooner and makes ownership visible. A deadline is not a guarantee that arbitrary code will stop: the called API or worker must observe the context or another bounded cancellation mechanism.

The next example supplies a deadline already in the past. Its error can therefore be checked without waiting or publishing an unstable elapsed time. Err distinguishes context.Canceled from context.DeadlineExceeded. A caller may wrap these errors with context while retaining errors.Is compatibility when that relationship is part of its contract.

go Inspect an already-expired deadline
package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
	defer cancel()
	fmt.Println("expired", ctx.Err() == context.DeadlineExceeded)
}
Expected output
expired true

The past deadline yields DeadlineExceeded without any sleep or timing measurement.

Make every blocking stage cancellable

A select can wait on ctx.Done alongside a channel send or receive. But it does not interrupt a blocking operation already called outside that select. Use context-aware APIs, deadlines, closing under a documented ownership protocol, or bounded operations as appropriate. Starting a goroutine around every uncancellable call can merely move the leak into the background.

When several select cases are ready, selection does not give cancellation absolute priority. Correct designs tolerate the allowed choice or explicitly check policy boundaries. A worker may process an item racing with cancellation; define whether that is permitted and when external effects become committed.

Preserve the reason at the correct boundary

Propagate the incoming context instead of replacing it with Background deep inside a request. Creating a fresh root discards the caller’s deadline and cancellation. For cleanup that must outlive the request, design a separate bounded lifecycle deliberately rather than accidentally detaching work. Cancellation causes are available in baseline-compatible context APIs when a richer reason is required.

Test cancellation before work begins, during a blocked send, during downstream failure, and while shutting down. Require a completion signal and correct state, not just a returned context error. A cancelled request can still leave leaked goroutines or partially committed effects if the lifetime protocol is incomplete.

Common errors and fixes

A cancelled request keeps doing downstream work.

Why it happens
An inner function replaced the parent with Background.
How to fix it
Pass the incoming context through each relevant call.
Prevent it next time
Test propagation from the outermost operation.

Cancel returns but workers still access released state.

Why it happens
Cancellation was mistaken for joining.
How to fix it
Wait for explicit completion before releasing shared resources.
Prevent it next time
Document both stop requests and stop acknowledgments.

A worker ignores cancellation while writing to a channel.

Why it happens
The blocking send has no cancellation alternative.
How to fix it
Use select with Done and define ownership of unsent data.
Prevent it next time
Review every potentially blocking operation.

Check your understanding

1. A function calls WithTimeout and returns early after success. Why must it still call cancel?

Show worked solution

The derived context may retain timer and parent relationships until cancellation or expiry. Calling cancel on all paths releases its resources promptly and communicates lifetime ownership. Use defer cancel immediately after successful derivation for a function-scoped operation. This does not replace waiting for any goroutine started under that context when the function owns that goroutine.

2. A worker uses select with both a ready result send and a cancelled Done channel. May the send occur? Define a robust contract.

Show worked solution

Yes, either ready case can be selected under the select rules. Permit a final racing result and let the receiver ignore it after cancellation, or design a stronger explicit commit protocol if the domain requires one. Do not claim cancellation priority from source order. In either design, ensure the worker returns and completion is observed.

Key takeaways

  • Context cancellation is cooperative and does not join workers.
  • Propagate the parent lifetime and cancel derived contexts promptly.
  • Each blocking stage and external effect needs a deliberate cancellation policy.

Official references