gonjure
A grimoire of go incantations

← back to the grimoire

Context

A leash of intent. Cancel a working spell from across the chamber.

grimoire fragment · main.go
package main

import (
	"context"
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

func main() {
	var (
	  ctx, cancel = context.WithTimeout(
	  	context.Background(),
	  	3500*time.Millisecond,
	  )

		count atomic.Int64

    wg    sync.WaitGroup
	)

  defer cancel()

	for idx := 0; idx < 3; idx++ {
		wg.Add(1)

		go func(idx int) {
			defer wg.Done()

			if _, err := scribe(ctx, fmt.Sprintf("#%d", idx), 2000); err == nil {
				count.Add(1)
			}
		}(idx)
	}

	wg.Wait()

	fmt.Printf("scribed %d, ctx err %v\n", count.Load(), ctx.Err())
}

// scribe is apprentice work day.
func scribe(ctx context.Context, name string, delay int) (string, error) {
	select {
	case <-ctx.Done():
		return "", ctx.Err()

	case <-time.After(time.Duration(delay) * time.Millisecond):
		return "scroll of " + name, nil
	}
}
The chamber and the leash
wizard · main context.Context apprentices · goroutines deadline
Dials & sigils

What you are witnessing

A context.Context is the wizard's leash of intent. A single value he weaves once and then hands to every apprentice he dispatches. Down its length flows two pieces threads: Are we still wanted? and How much longer?. A broadcast switch (ctx.Done()) snaps loudly the instant the wizard either explicitly cancel()s or the hourglass empties. Every apprentice that bothered to listen hears the snap simultaneously and abandons scribing.

In the workshop, a wizard holds his wand end of a glowing leash that descends to a hexagonal context.Context knot. It splits into one strand per hunched over apprentice, each scribing dutifully. Should the deadline arrive before work completion, the hourglass will empty with a crack of lightning running along the trunk. Its strands shatter as every candle is snuffed at the same instant, causing each apprentice to freeze from being startled. That is cancellation. It is not them deciding to stop but the wizard's deciding for them, all at once from across the chamber.

Without such a leash, it descends into an ungoverned equivalent. There is no hourglass or leash causing the wizard to stand while the apprentices scribe blissfully on and the program would be at the mercy of whichever took longest. If one of them ran forever then so would main.

Why a wizard cares

An http handler with db query which takes takes a few seconds seconds will block a goroutine with conn and slot in whichever pool was lent to it. While it dawdles, every other caller waits in a longer queue. context is Go's standard issue answer this work is bound to that request and when the request goes away the work should as well. Many stdlib functions that can take a long time accept context.Context for exactly this reason.

Try it! Push spell duration well past deadline and watch every apprentice get yanked at the same instant. Slide the deadline back out past the spell duration and the same apprentices finish in time. Sparkles bloom and scrolls are ready.

Shape of the spell

  • Pass it as the first arg. Convention, lint and stdlib apis agree that ctx context.Context comes first. Don't store in a struct unless the struct's lifetime is the request's lifetime and even then, think twice because someone will question it.
  • Always pair WithCancel / WithTimeout / WithDeadline with defer cancel(). These constructors return a cancel function. Calling it releases the timer and any associated goroutine. Skipping it is a slow leak that lint will mutter about and your profile will eventually shout about.
  • Honour the leash with select. Include a case <-ctx.Done(): branch and exit with ctx.Err() Anywhere you sleep, block or wait on a channel with long running work. A goroutine that ignores its context will keep working long after main has packed up and left.
  • Hand it on, unchanged or derived. Inner calls get the same ctx or a child made from it with a tighter deadline / extra value. Cancelling a parent cancels every descendant. The leash is rooted at the wizard rather than apprentice.
  • Values are request scoped, not parameters. context.WithValue is fine for the trace id and authenticated user but is not a substitute for function args and the values are typed only as any.

Pitfalls

  • Sleeping with time.Sleep ignores the leash. It cannot be cancelled in the middle of a nap. Use select with time.After and ctx.Done() or time.NewTimer you can Stop().
  • Forgetting defer cancel() leaks timers and goroutines. Even a WithCancel that you know was already cancelled deserves the defer, belt and wand strap.
  • Errors matter. Leverage ctx.Err(). Don't pretend cancellation is a generic error. Get the distinction.
  • Cancellation is cooperative. Apprentices that don't listen don't stop. A goroutine will happily keep computing past ctx.Done(). Sprinkle in a check or break the loop into yielding pieces.
  • Background and todo contexts are not interchangeable. context.Background() is the root of the leash for main, tests and top level handlers. context.TODO() is a flag to your future self that you meant to thread a real context through here and haven't yet. Static analysis tools tell them apart and you should too.

Reagents required: the context package, in your standard pouch. Do remember the defer cancel().