context: cancellation and deadlines

Go · Practical · 12 min

Every server, client and long-running operation in Go is expected to accept a context and stop when it's done.

A context.Context carries a cancellation signal, an optional deadline and request-scoped values down a call chain. By convention it is the first parameter, named ctx: func Fetch(ctx context.Context, url string) error.

Contexts form a tree. Start from context.Background() (in main, tests, top-level) and derive children:

  • ctx, cancel := context.WithCancel(parent) — cancelled when you call cancel().
  • context.WithTimeout(parent, 2*time.Second) / WithDeadline — cancelled automatically at the deadline.
  • Cancelling a parent cancels all its children.

Always `defer cancel()` right after creating a derived context; it releases the timer and resources even when the work finishes early. go vet warns if you forget.

Code doing the work checks for cancellation:

  • <-ctx.Done() is a channel that closes on cancellation — use it in a select.
  • ctx.Err() then returns context.Canceled or context.DeadlineExceeded.
  • Pass ctx on to everything that blocks: http.NewRequestWithContext, database queries, your own functions.

context.WithValue attaches request-scoped data such as a trace ID, using an unexported key type to avoid collisions. Don't use it to pass optional function parameters.

Don't store contexts in structs; pass them explicitly. Use context.TODO() as a placeholder when you haven't plumbed one through yet. Go 1.21 added context.WithoutCancel and context.AfterFunc for advanced cases.

Example

package main

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

func work(ctx context.Context, steps int) error {
	for i := range steps {
		select {
		case <-ctx.Done():
			return fmt.Errorf("stopped at step %d: %w", i, ctx.Err())
		case <-time.After(20 * time.Millisecond):
			// one unit of work done
		}
	}
	return nil
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()

	err := work(ctx, 10)
	fmt.Println(err) // stopped at step 2: context deadline exceeded
}

Remember

  • Pass ctx context.Context as the first parameter, never store it
  • Derive with WithCancel/WithTimeout and always defer cancel()
  • Blocking code selects on ctx.Done() and returns ctx.Err()

Watch out for

  • Ignoring ctx inside a loop or blocking call, so cancellation never actually stops the work.

Check yourself

  1. A parent context is cancelled. What happens to a child created with context.WithTimeout(parent, time.Hour)?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pkg.go.dev/context