Irbisa · Cheatsheet

Context & Cancellation

The context tree, timeouts, deadlines, values, cancellation propagation

Middle Developer · 21 questions

  1. 01

    What is context.Context in Go, what is it used for, and which constructors does the context package provide?

    A context.Context carries a cancellation signal, an optional deadline and request-scoped values across API boundaries and goroutines, so the whole call tree of one request can be stopped together.

    func handle(w http.ResponseWriter, r *http.Request) {	// A child of the request context with a 2 s budget.	ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)	defer cancel() 	rows, err := db.QueryContext(ctx, "SELECT id FROM orders WHERE user_id = $1", userID(r))…
  2. 02

    What is the difference between context.Background() and context.TODO()?

    At runtime there is none: both return an empty context that is never cancelled, has no deadline and holds no values.

  3. 03

    What does this program print, and what does it show about how cancellation flows between parent and child contexts?

    func main() {	parent, cancelParent := context.WithCancel(context.Background())	child, cancelChild := context.WithCancel(parent) 	cancelChild()	fmt.Println(parent.Err(), child.Err()) 	child2, cancelChild2 := context.WithTimeout(parent, time.Hour)	defer cancelChild2() 	cancelParent()	fmt.Println(parent.Err(), child2.Err())}

    It prints <nil> context canceled, then context canceled context canceled: cancelling a child never touches its parent, while cancelling a parent cancels every context derived from it.

  4. 04

    What is the difference between ctx.Done() and ctx.Err(), and which errors can Err() return?

    Done() returns a channel that is closed when the context ends, so you can wait on it in a select; Err() says why it ended (context.Canceled or context.DeadlineExceeded) and is nil while the context is alive.

    func worker(ctx context.Context, jobs <-chan Job) error {	for {		select {		case <-ctx.Done():			return ctx.Err() // context.Canceled or context.DeadlineExceeded		case j := <-jobs:…
  5. 05

    Why must you always call the cancel function returned by context.WithTimeout or context.WithCancel?

    cancel releases what the derived context holds: it unregisters the child from its parent and stops its timer.

    // wrong: vet (lostcancel) reports the discarded cancelfunc fetchBad(parent context.Context, id string) (*User, error) {	ctx, _ := context.WithTimeout(parent, time.Second)	return get(ctx, id)}…
  6. 06

    Why should context.Context be passed as a function's first parameter rather than stored in a struct, and why should you never pass nil?

    A context belongs to one call, not to an object: as the first parameter ctx it makes each call's lifetime explicit, while a stored one ties every later call to the deadline, cancellation and values of whichever request created the struct.

  7. 07

    Does cancelling a context stop the goroutine that uses it? What must the goroutine itself do?

    No. Cancellation only closes the Done() channel and sets Err(); Go cannot kill a goroutine from outside, so it stops only when its own code notices the signal and returns.

    // wrong: time.Sleep ignores ctx, so the loop runs on after cancelfunc pollBad(ctx context.Context) {	for {		check()		time.Sleep(10 * time.Second)	}…
  8. 08

    When do you use context.WithCancel, context.WithTimeout and context.WithDeadline?

    WithCancel when your own code decides when to stop, WithTimeout when an operation gets a relative budget ("at most 2 s"), and WithDeadline when you already hold an absolute point in time that several steps must share.

  9. 09

    Your repository wraps every query in context.WithTimeout(ctx, 5*time.Second), yet in production queries abort after 300 ms. Where does 300 ms come from?

    From the parent: the incoming request had only 300 ms left, and a derived context can never outlive its parent; WithTimeout effectively keeps the earlier of the two deadlines.

    func (r *Repo) Load(ctx context.Context, id int64) (*Row, error) {	// A ceiling on what is left, not a grant of five seconds.	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)	defer cancel() 	if dl, ok := ctx.Deadline(); ok && time.Until(dl) < 20*time.Millisecond {…
  10. 10

    The cache lookup below should get 100 ms and the database up to 2 s, yet h.db.Load fails with context deadline exceeded after 100 ms. Why?

    func (h *Handler) Get(ctx context.Context, id string) (*Page, error) {	ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)	defer cancel() 	if p, err := h.cache.Get(ctx, id); err == nil {		return p, nil	}	return h.db.Load(ctx, id) // the query needs about 300 ms}

    ctx, cancel := context.WithTimeout(ctx, ...) rebinds ctx, so the database call receives the 100 ms cache context, and no call can get more time than the context it is handed.

  11. 11

    What does this program print, and how should you define context keys so that two packages cannot collide?

    type key string func main() {	ctx := context.WithValue(context.Background(), key("user"), "alice")	ctx = context.WithValue(ctx, "user", "bob") 	fmt.Println(ctx.Value("user"))	fmt.Println(ctx.Value(key("user")))	fmt.Println(ctx.Value("tenant"))}

    It prints bob, alice, <nil>: Value walks up the chain and compares keys with ==, and keys of different types are never equal, even when they hold the same text.

  12. 12

    How should retries and error handling treat context.Canceled differently from context.DeadlineExceeded?

    Canceled means the caller is gone, so stop at once and do not count it as a failure; DeadlineExceeded means the work was too slow (your latency problem), and a retry makes sense only while the overall context still has time left.

    func retry(ctx context.Context, attempts int, f func(context.Context) error) error {	var err error	for i := range attempts {		actx, cancel := context.WithTimeout(ctx, time.Second) // per-attempt budget		err = f(actx)		cancel()…
  13. 13

    Find the bug: this queue worker's heap grows steadily for as long as it runs, although every job finishes in milliseconds.

    func (w *Worker) Run(ctx context.Context) error {	for {		select {		case <-ctx.Done():			return ctx.Err()		case job := <-w.jobs:			jobCtx, cancel := context.WithTimeout(ctx, 30*time.Second)			defer cancel()			if err := w.handle(jobCtx, job); err != nil {				slog.ErrorContext(ctx, "job failed", "id", job.ID, "err", err)			}		}	}}

    defer cancel() runs when Run returns, not at the end of the iteration, and Run returns only at shutdown, so every job leaves a deferred call holding its context and timer, and the heap grows with the number of jobs processed.

  14. 14

    What happens to r.Context() when the HTTP client disconnects, and how do you make the handler's outbound calls stop with it?

    net/http cancels the request context when the client's connection closes, when an HTTP/2 stream is reset, or when ServeHTTP returns, but only work that was given that context stops.

    var client = &http.Client{} // shared; no Client.Timeout, which would cut long streams func proxy(w http.ResponseWriter, r *http.Request) {	// http.NewRequest would use context.Background() and ignore the disconnect.	req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, upstream+r.URL.Path, nil)	if err != nil {…
  15. 15

    Find the bug: under load, this service's goroutine count keeps climbing whenever the upstream gets slow.

    func fetch(ctx context.Context, url string) ([]byte, error) {	ctx, cancel := context.WithTimeout(ctx, 2*time.Second)	defer cancel()	ch := make(chan result)	go func() {		body, err := download(url) // legacy client: takes no context		ch <- result{body, err}	}() 	select {	case r := <-ch:		return r.body, r.err	case <-ctx.Done():		return nil, ctx.Err()	}}

    The channel is unbuffered, so when the timeout wins the select, nobody ever receives and the worker goroutine blocks on ch <- forever: one leaked goroutine, with its stack and the downloaded body, per timed-out call.

  16. 16

    What does context.WithoutCancel do, and when do you need it?

    context.WithoutCancel(ctx) (Go 1.21) returns a context that keeps the parent's values but is never cancelled and has no deadline, meant for work that must outlive the request yet still log with its trace id and tenant.

    func handler(w http.ResponseWriter, r *http.Request) {	// wrong: go audit.Write(r.Context(), ev) is cancelled when the handler returns 	// right: keep the values, drop the cancellation, add our own bound	ctx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), 5*time.Second)	ev := eventFrom(r) // read r now, not after the handler has returned…
  17. 17

    How is the context package implemented internally: how does cancellation reach the children, and why is Value lookup linear?

    Each With* call wraps its parent in a small struct, so contexts form a linked tree: a cancellable node keeps a set of its children and, when cancelled, closes its channel and cancels them in turn, while Value simply asks each parent up the chain.

  18. 18

    Every failure in a fan-out reports just context canceled. How do you make the cancellation carry its reason: which shard failed, which budget ran out?

    Use context.WithCancelCause (Go 1.20): cancel(err) records the reason and context.Cause(ctx) returns it on that context and every descendant, while ctx.Err() stays the plain context.Canceled that existing checks rely on.

    var ErrSearchBudget = errors.New("search budget exhausted") func search(parent context.Context, q string) ([]Hit, error) {	ctx, cancel := context.WithTimeoutCause(parent, 200*time.Millisecond, ErrSearchBudget)	defer cancel()…
  19. 19

    A third-party client has Close() but takes no context.Context. How do you abort it on cancellation without parking a goroutine per call?

    Register a callback with context.AfterFunc(ctx, f) (Go 1.21): f runs in its own goroutine once ctx is done, and nothing sits waiting in the meantime.

    // right: a registered callback instead of a waiting goroutinefunc call(ctx context.Context, c *vendor.Client) error {	stop := context.AfterFunc(ctx, func() {		c.Close() // runs in its own goroutine when ctx ends	})	err := c.Run()…
  20. 20

    Does srv.Shutdown(ctx) cancel the contexts of in-flight requests? How do you make long-running handlers stop during shutdown?

    No. Shutdown closes the listeners and idle connections, then waits for active requests to finish; its ctx only bounds how long you wait, and when it expires the handlers keep running with a live r.Context().

    func main() {	sigCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)	defer stop() 	base, cancelBase := context.WithCancel(context.Background())	defer cancelBase()…
  21. 21

    Service A gives B 500 ms, but B calls C with a fixed 2 s timeout, and C burns CPU on requests nobody waits for. How do you propagate deadlines across service hops?

    A context does not cross the network by itself, so each hop must send its remaining budget and the next hop must turn it back into a deadline; then work stops everywhere once the original caller has given up.

    const hopHeadroom = 50 * time.Millisecond // Caller: put the remaining time on the wire, never an absolute timestamp.func forward(ctx context.Context, req *http.Request) (*http.Response, error) {	if dl, ok := ctx.Deadline(); ok {		budget := time.Until(dl) - hopHeadroom…