The context Package

One value that carries a cancellation signal, a deadline and request-scoped data through every call in a request.

Syntaxfunc Load(ctx context.Context, id int) (Order, error)

A context.Context is a value that travels with a piece of work and answers one question: should this work still be happening? It also carries a deadline when there is one, and a small set of values tied to the request. A deadline is a point in time after which the work should stop.

It exists because work is usually started on behalf of someone who can go away. A user closes the page. A caller gives up after two seconds. Without a shared signal the goroutines doing that work keep running, holding memory and connections, with nobody left to read the answer.

The interface

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key any) any
}

Four methods, and no method that changes anything. A context is read-only once you have it.

Where one comes from

context.Background() returns the root context. It is never cancelled and has no deadline. context.TODO() behaves identically and marks a place where a real context should be threaded through later.

Every other context is derived. context.WithCancel, context.WithTimeout, context.WithDeadline and context.WithValue each take a parent and return a child that points at it. The parent is unchanged.

The convention

The context is the first parameter and it is named ctx, as in func Load(ctx context.Context, id int) (Order, error). Pass it down the call chain instead of keeping it in a struct field.

Asking whether to stop

Two forms, the same information. <-ctx.Done() blocks until the work should stop. ctx.Err() != nil is the shorthand for a select on ctx.Done() with a default case: it answers right now without waiting.

Example

Example · go
package main

import (
	"context"
	"errors"
	"fmt"
)

// greet does a small piece of work, but only for a caller who is still waiting.
func greet(ctx context.Context, name string) (string, error) {
	// ctx.Err() is nil while the work is still wanted.
	if err := ctx.Err(); err != nil {
		return "", err
	}
	return "hello, " + name, nil
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	// defer runs cancel when main returns, releasing the context.
	defer cancel()

	msg, err := greet(ctx, "ada")
	fmt.Println(msg, err)

	// The caller gives up on the work.
	cancel()

	msg, err = greet(ctx, "ada")
	fmt.Printf("%q %v\n", msg, errors.Is(err, context.Canceled))
}

// Output:
// hello, ada <nil>
// "" true

When to use it

  • Stop a goroutine once the caller that started it no longer needs the result.
  • Give a database query a two-second limit instead of letting it hang forever.
  • Carry a request id so every log line for one request can be matched up later.
  • Cancel every outstanding sub-task when a worker process starts shutting down.

More examples

Threading one context through a call chain

A context is only useful if it reaches the thing that actually blocks: the socket read, the query, the lock wait. That means every function between the top of the request and the blocking call has to accept it and hand it on. Add the parameter even to functions that do not look at ctx themselves, because the function they call might.

Example · go
// Every function takes ctx first and passes the same value on, unchanged.
func handleOrder(ctx context.Context, id int64) error {
	order, err := loadOrder(ctx, id)
	if err != nil {
		return err
	}
	return chargeCard(ctx, order.CardToken, order.TotalPence)
}

func loadOrder(ctx context.Context, id int64) (Order, error) {
	// ctx carries on into the database call, which will abandon its query
	// if ctx is cancelled while it waits.
	return queryOrder(ctx, id)
}

context.TODO() as a visible placeholder

Reach for this while you are retrofitting contexts into code that does not have them. It behaves exactly like context.Background(), so nothing is ever cancelled, but the name records that the wiring is unfinished. Use Background() for the genuine top of a program, such as func main, and TODO() for a gap you intend to close.

Example · go
// chargeCard needs a context, but nothing above this function has one yet.
// context.TODO() compiles today and is easy to grep for tomorrow.
func settleYesterday() error {
	return chargeCard(context.TODO(), "tok_123", 1999)
}

The same cancellation check written both ways

These two loops do the same thing. The default case makes the select give up immediately when Done() is not closed, which is exactly what ctx.Err() reports. Use the short form for a loop over work you already have in hand, and keep the select for cases where you need to wait on Done() alongside a channel.

Example · go
// Shorthand: a non-blocking check at the top of each iteration.
func processAll(ctx context.Context, rows []Row) error {
	for _, row := range rows {
		if err := ctx.Err(); err != nil {
			return err
		}
		process(row)
	}
	return nil
}

// Longhand: the same check as a select with a default case.
func processAllLong(ctx context.Context, rows []Row) error {
	for _, row := range rows {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}
		process(row)
	}
	return nil
}

Discussion

  • Be the first to comment on this lesson.