Context
A leash of intent. Cancel a working spell from across the chamber.
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
}
} 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.Contextcomes 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/WithDeadlinewithdefer 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 acase <-ctx.Done():branch and exit withctx.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
ctxor 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.WithValueis fine for the trace id and authenticated user but is not a substitute for function args and the values are typed only asany.
Pitfalls
- Sleeping with
time.Sleepignores the leash. It cannot be cancelled in the middle of a nap. Useselectwithtime.Afterandctx.Done()ortime.NewTimeryou canStop(). - Forgetting
defer cancel()leaks timers and goroutines. Even aWithCancelthat you know was already cancelled deserves thedefer, 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 formain, 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().