Go path · Many things at once · Topic 12 of 13

context: cancellation and deadlines

Not started

A context.Context carries a cancellation signal, an optional deadline and a few request-scoped values across API boundaries and goroutines. A server creates one per request; each function passes it to the functions it calls; and when the request is abandoned, because the client went away, a deadline passed or a sibling failed, every piece of work using that context can see it and stop. Nothing is stopped by force: cancellation is a signal that code has to check.

Why it matters: When a client disconnects or a deadline passes, every goroutine and outbound call working on that request should stop. context is the standard way to say so, and every network library accepts one.

  1. 1A tree of contexts

    Each request gets a context; WithCancel, WithTimeout and WithDeadline derive children. Cancelling a parent cancels all of its children.

  2. 2Done is a channel

    ctx.Done() is closed on cancellation, so it fits in a select. ctx.Err() then says why, Canceled or DeadlineExceeded.

  3. 3Always call cancel

    The cancel function releases the child's timer and its link to the parent. Defer it right away; go vet reports when you forget.

  4. 4First parameter, never a field

    Pass ctx explicitly as the first argument of each function that needs it, rather than storing it in a struct.

1

How it works

The explanation, with examples you can run.

Deriving and cancelling

package main import (	"context"	"errors"	"fmt"	"time") func fetch(ctx context.Context) (string, error) {	select {	case <-time.After(200 * time.Millisecond): // pretend this is a slow call		return "data", nil	case <-ctx.Done():		return "", ctx.Err()	}} func main() {	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)	defer cancel() // always release the timer and children 	_, err := fetch(ctx)	fmt.Println(err)	fmt.Println(errors.Is(err, context.DeadlineExceeded)) 	parent, stop := context.WithCancel(context.Background())	child, cancelChild := context.WithTimeout(parent, time.Hour)	defer cancelChild()	stop() // cancelling the parent cancels every child	<-child.Done()	fmt.Println(child.Err())}

It prints context deadline exceeded, true and context canceled.

  • context.Background() is the root, for main, tests and start-up. In a server, use r.Context() from the request instead; net/http cancels it when the client disconnects or the handler returns.
  • A child can only shorten its parent's deadline, never extend it. A one-hour timeout under a parent with 50 ms left still ends in 50 ms.
  • Done() returns a channel that is closed on cancellation, so a select can wait on it next to real work. After it is closed, Err() returns context.Canceled or context.DeadlineExceeded.
  • Calling the cancel function releases resources. Forgetting it leaks the child until the parent ends; go vet's lostcancel check catches most cases.

Using it well

  • Take ctx context.Context as the first parameter, and don't store it in a struct: a context belongs to one operation.
  • Pass it to everything that can block: http.NewRequestWithContext, db.QueryContext, gRPC calls. Those libraries close connections or abort queries when it is cancelled.
  • Set one deadline at the edge (when the request arrives, or before a batch job's call) and let it flow down. Per-call timeouts deeper in should only make it shorter.
  • In a loop that does work in steps, check ctx.Err() between steps, or select on ctx.Done() next to each blocking send, so a cancelled job stops within one step rather than running to the end.
  • errgroup.WithContext (in golang.org/x/sync/errgroup) runs several goroutines and cancels their shared context as soon as one returns an error: the usual way to fan out calls and fail fast.
  • context.WithValue is for request-scoped data that crosses API boundaries, such as a trace ID. It is not a way to pass optional parameters.

Newer helpers: WithCancelCause and context.Cause (Go 1.20) record why something was cancelled; WithoutCancel (Go 1.21) keeps values but drops cancellation, useful for clean-up that must finish after the request ends; AfterFunc (Go 1.21) runs a function when a context is done.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.A handler derives ctx with a 2-second timeout and passes it to three calls in a row. The first takes 1.5 s. How long does the second have?
  2. 2.What does ctx.Err() return after context.WithTimeout's deadline has passed?
  3. 3.Which is the right way to make a goroutine stop when its context is cancelled?

0 of 3 right so far. Get all three to finish this topic.