Language Internals
Part 8 of 11 · Go Language ProficiencyContext, Cancellation & Concurrency Patterns
context.Context, cancellation, errgroup, and patterns that stop sibling work.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Question ladder
L1
Where does ctx go in the signature?
Answer
First, by convention, on every function that can block or start work.
L2
What does WithTimeout return?
Answer
A child context and a cancel function. Always defer cancel so the timer is released.
L3
Does cancel stop the goroutine?
Answer
No. It closes Done. The goroutine must select on it and return.
L4
What should a canceled function return?
Answer
ctx.Err, which is Canceled or DeadlineExceeded.
L5
What does errgroup.WithContext add?
Answer
The first error cancels the derived context, so siblings can observe it, and Wait returns that error.
L6
How is that different from a WaitGroup?
Answer
WaitGroup only counts. It does not carry an error or a cancel.
L7
What does go test -race catch?
Answer
Concurrent access to the same memory where at least one access is a write, without a happens-before edge.
Failure modes
Cancel ignored
The client disconnects and the goroutine keeps the socket until the process exits.
cancel not deferred
The timeout context leaks its timer when the function returns early.
Unbounded fan-out
One goroutine per URL with no semaphore and no errgroup.
Value as a parameter bag
context.WithValue hides required arguments that belonged in the signature.
Misconceptions
Cancel kills the goroutine.
It is a signal. Code that never reads Done cannot notice.
A derived timeout cancels the parent.
Cancel flows down to children, not up to the parent.
errgroup replaces a mutex.
It joins fallible goroutines. Shared maps still need a lock.
The race detector is a linter you can skip.
It instruments memory access. Turn it on for packages that share state.
Interviewer traps
Saying the runtime preempts a canceled goroutine.
Show the select on Done and the return of ctx.Err.
Storing a database handle in context values.
Pass the handle as a parameter. Values are for request metadata.
Using WaitGroup when the first error should stop siblings.
Name errgroup.WithContext.
Design scenario
Same prompt for every reader.
Requirements
r.Context as the parent, errgroup.WithContext, both fetches select on Done, go test -race in CI.
Traffic / scale
Each request starts two outbound calls.
Latency
A timeout child bounds the wait. A leaked goroutine does not.
Consistency
The handler returns the first error and does not publish a partial result.
Availability
Sibling calls must see the cancel or they hold connections.
Failure assumptions
- Cancel is assumed to be preemptive.
- A WaitGroup is enough to stop the other call.
Constraints
- Stay on context and errgroup.
- Do not restate another language's scheduler.
Prompt
A handler fans out two downstream calls and must stop the second when the first fails or the client disconnects.
A signal the callee must read
Prefer
Context down the stack
The parent cancels. Children see Done. Work returns ctx.Err.
- Timeouts are children.
- errgroup cancels siblings.
- The race detector is a test flag.
Alternative
A signal nobody reads
AbortSignal and asyncio timeouts also do nothing if the callee never checks them.
- The host still has to observe the signal.
- TaskGroup cancels siblings on error.
- A forgotten check leaks the task.
Overview
Pass context.Context first. Derive deadlines with WithTimeout and always defer cancel(). Cancellation closes Done. It does not preempt the goroutine. errgroup is the pattern that ties fan-out to that signal.
Decisions
- 1
Step 1 Request arrives with ctx
- nextStep 2 ctx, cancel := context.WithTimeout - defer cancel
- 2
Step 2 ctx, cancel := context.WithTimeout - defer cancel
- nextStep 3 errgroup.WithContext fans out workers
- 3
Step 3 errgroup.WithContext fans out workers
- nextStep 4 A worker fails or the deadline hits
- ?
Step 4 A worker fails or the deadline hits
- noStep 5a g.Wait returns nil
- yesStep 5b ctx is canceled for every sibling
- 5
Step 5a g.Wait returns nil
- 6
Step 5b ctx is canceled for every sibling
- nextStep 6 Workers see ctx.Done and return ctx.Err
- worker ignores ctx.DoneFailure path - leak; cancellation is cooperative
- 7
Step 6 Workers see ctx.Done and return ctx.Err
- nextStep 7 g.Wait returns the first error
- 8
Step 7 g.Wait returns the first error
- 9
Failure path - leak; cancellation is cooperative
Lesson map
Context, Cancellation & Concurrency Patterns
context.Context, cancellation, errgroup, and patterns that stop sibling work.
Architecture. Step 1 Request arrives with ctx Ready. Step 2 ctx, cancel := context.WithTimeout - defer cancel Ready. Step 3 errgroup.WithContext fans out workers Ready. Step 4 A worker fails or the deadline hits Ready. Step 5a g.Wait returns nil Ready. Step 5b ctx is canceled for every sibling Ready. Step 6 Workers see ctx.Done and return ctx.Err Ready. Step 7 g.Wait returns the first error Ready. Failure path - leak; cancellation is cooperative Ready
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB A["Step 1 Request arrives with ctx Ready"] B["Step 2 ctx, cancel := context.WithTimeout - defer cancel Ready"] C["Step 3 errgroup.WithContext fans out workers Ready"] D["Step 4 A worker fails or the deadline hits Ready"] E["Step 5a g.Wait returns nil Ready"] F["Step 5b ctx is canceled for every sibling Ready"] G["Step 6 Workers see ctx.Done and return ctx.Err Ready"] H["Step 7 g.Wait returns the first error Ready"] X["Failure path - leak cancellation is cooperative Ready"] A -->|continues| B B -->|continues| C C -->|continues| D D -->|no| E D -->|yes| F F -->|continues| G G -->|continues| H F -->|worker ignores ctx.Done| X
Press Run. Snippets must be self-contained — no network, files, or native modules.
The flag stands in for ctx.Done. If work never checked it, cancel would not matter.
Rosetta — timeout
package ctxdemo
import (
"context"
"time"
)
func Work(parent context.Context) error {
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(100 * time.Millisecond):
return nil
}
}export async function work(signal?: AbortSignal) {
const ctrl = AbortSignal.any([
signal ?? new AbortController().signal,
AbortSignal.timeout(2000),
]);
await new Promise((resolve, reject) => {
const timer = setTimeout(resolve, 100);
ctrl.addEventListener("abort", () => {
clearTimeout(timer);
reject(ctrl.reason);
});
});
}import asyncio
async def work() -> None:
async with asyncio.timeout(2.0):
await asyncio.sleep(0.1)defer cancel() releases the timer even when the work finishes early. ctx.Err() is context.Canceled or context.DeadlineExceeded. AbortSignal and asyncio.timeout are the same idea on another runtime.
Rosetta — errgroup
package eg
import (
"context"
"golang.org/x/sync/errgroup"
)
func FetchBoth(ctx context.Context) error {
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return fetchA(ctx) })
g.Go(func() error { return fetchB(ctx) })
return g.Wait()
}
func fetchA(context.Context) error { return nil }
func fetchB(context.Context) error { return nil }export async function fetchBoth(parent: AbortSignal) {
const ctrl = new AbortController();
const signal = AbortSignal.any([parent, ctrl.signal]);
const fail = (err: unknown) => {
ctrl.abort(err);
throw err;
};
try {
await Promise.all([fetchA(signal).catch(fail), fetchB(signal).catch(fail)]);
} finally {
ctrl.abort();
}
}
async function fetchA(_signal: AbortSignal) {}
async function fetchB(_signal: AbortSignal) {}import asyncio
async def fetch_both() -> None:
async with asyncio.TaskGroup() as tg:
tg.create_task(fetch_a())
tg.create_task(fetch_b())
async def fetch_a() -> None:
return None
async def fetch_b() -> None:
return NoneWithContext cancels the sibling context when one Go function returns an error. Promise.all alone does not cancel the other fetch. The TypeScript sample aborts siblings on the first failure with AbortController and AbortSignal.any. asyncio.TaskGroup cancels the rest. A plain WaitGroup does neither.
Do not stash required dependencies in context.Value. Pass them as parameters. Values are for a request id that logging already understands.
go test -race reports unsynchronized writes. Run it on packages that share maps or slices across goroutines. Go 1.27.1 does not make cancel preemptive. The goroutineleak profile is how production finds a goroutine that blocked forever. That profile is on the last page of this series.
Interview Q&A
Does cancel kill a goroutine?
Answer
No. It signals Done. The goroutine returns only if it selects on that channel or calls something else that does.
Why is ctx the first parameter?
Answer
So every blocking API looks the same, and you do not hide cancellation behind a struct field.
What do you return after Done?
Answer
ctx.Err. That preserves canceled versus deadline exceeded.
Why defer cancel on a timeout?
Answer
The child holds a timer. Leaving without cancel leaks it until the deadline fires.
errgroup or WaitGroup?
Answer
errgroup when a failure should cancel siblings and surface one error. WaitGroup when you only need a count.
Does a child cancel the parent?
Answer
No. WithTimeout cancels the child. The parent stays live unless something cancels the parent.
What does the race detector not do?
Answer
It does not prove the absence of logic bugs. It reports conflicting memory access it observed during that test.
Where do AbortSignal and TaskGroup live in this site?
Answer
On the JS/TS and Python hubs. This page does not restate those runtimes.
Pitfalls
- Starting
gowithout passing the context in. - Checking cancel only in the handler and not in the client call.
- A worker pool with no limit.
- Hiding a database in
context.Value.
Two fetches share a handler context. Say which API cancels the second when the first returns an error, and what the second must read to notice.