Back to Notes

Go Context & Cancellation

context.Context — Cancellation, Deadlines, Values

What: The standard mechanism for propagating cancellation, deadlines, and request-scoped values down a call tree. Cooperative: work stops only where code checks ctx.Done().

Syntax/Signature: ctx, cancel := context.WithCancel(parent); WithTimeout(parent, d); WithDeadline(parent, t); WithCancelCause + context.Cause(ctx) (1.20); WithoutCancel, AfterFunc (1.21). Always defer cancel().

func fetchAll(ctx context.Context, urls []string) error {
	ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
	defer cancel() // releases timer + subtree even on success
	g, ctx := errgroup.WithContext(ctx)
	for _, u := range urls {
		g.Go(func() error {
			req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
			if err != nil { return err }
			resp, err := http.DefaultClient.Do(req)
			if err != nil { return err }
			return resp.Body.Close()
		})
	}
	return g.Wait()
}

How it works under the hood: Contexts form a tree; each cancelCtx registers itself with the nearest cancellable ancestor. Done() returns a lazily-created channel that cancellation closes — that's why one cancel signals any number of waiters. Cancelling a parent walks and cancels all children. Timer contexts wrap a time.Timer that fires cancel(DeadlineExceeded). Not calling cancel keeps the child (and its timer) reachable from the parent → memory/timer leak until the parent dies.

Interview angle: "Does cancelling a context kill the goroutine?" — No. Nothing kills a goroutine from outside; the goroutine must select on ctx.Done() and return. Also: "context in struct or parameter?" — parameter, first position, named ctx (structs pin one lifetime to another and break the tree).

Gotchas / common wrong answers:

  • Forgetting defer cancel()go vet flags it; leaks child contexts.
  • Stuffing dependencies (loggers, DB handles) into ctx.Value — values are for request-scoped cross-cutting data (trace IDs, auth), not DI. See [[SOLID Principles]]'s Dependency Inversion for what DI actually means, and [[Go Embedding & Dependency Injection]] for Go's idiomatic constructor-injection alternative.
  • Checking ctx.Err() without also making blocking calls cancellable — a blocked ch <- v ignores context unless it's inside a select with ctx.Done() (see [[Go Channels]]).

Trade-offs / when NOT to use: Detached background work spawned from a request should use context.WithoutCancel(ctx) (keeps values/trace, drops cancellation) — passing context.Background() loses tracing.

Interview Drills

  1. Implement "call fetch with a 2s timeout" so that no goroutine leaks even when the timeout fires.
func fetchWithTimeout(ctx context.Context) (Data, error) {
	ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
	defer cancel()
	ch := make(chan Data, 1) // buffered: the send below can never block
	go func() { ch <- fetch() }()
	select {
	case d := <-ch:
		return d, nil
	case <-ctx.Done():
		return Data{}, ctx.Err()
	}
}

The buffer of 1 is the load-bearing detail: on timeout the abandoned goroutine still completes its send, exits, and the channel+value are GC'd. With an unbuffered channel it would park forever (see [[Go Goroutine Leaks]]). Even better: make fetch itself take ctx so the work stops, not just the wait.

  1. Why doesn't cancelling a context actually kill the goroutine that's supposed to stop? — Go has no external mechanism to terminate a goroutine — cancellation is purely cooperative signaling via a closed channel (Done()); the goroutine itself must be written to select on that channel and return. A goroutine that never checks ctx.Done() (or is blocked on a channel op with no ctx-aware select) simply ignores cancellation entirely.
  2. Why would you use context.WithoutCancel(ctx) for a background task spawned from a request handler? — The task needs to keep running after the request itself completes/is cancelled (e.g., a fire-and-forget audit log write), but should still carry the request's trace ID and other values for observability — WithoutCancel preserves the value chain while detaching from the parent's cancellation signal, unlike context.Background() which loses both.

Cross-links

[[Go Channels]] · [[Go Goroutine Leaks]] · [[Go Embedding & Dependency Injection]] · [[SOLID Principles]]