Context
Cancelling work that nobody is waiting for any more.
- Write a function that takes
ctxfirst and returnsctx.Err()when it is cancelled. - Explain why cancelling a context does nothing unless your code checks
ctx.Done(). - Predict whether a child context made with
context.WithoutCancelstops when its parent is cancelled.
What is context used for in Go?
context.Context is how a Go program says "stop, nobody needs this any more". In a typical service it is the first parameter of every function that does I/O: database queries, HTTP calls, RPCs.
func fetch(ctx context.Context, url string) error { ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() // ALWAYS, even on success — or you leak the timer req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return err } resp, err := http.DefaultClient.Do(req) // aborts when ctx is done if err != nil { return err } defer resp.Body.Close() return nil}func work(ctx context.Context) error { select { case <-time.After(5 * time.Second): return nil case <-ctx.Done(): return ctx.Err() // context.Canceled or context.DeadlineExceeded }}What does context.WithoutCancel do?
It gives you a context that keeps the parent’s values but is never cancelled. Sometimes work must outlive the request that started it, such as writing an audit log after the response is sent. If you pass r.Context(), the log write is cancelled the moment the client disconnects. context.WithoutCancel(ctx) (Go 1.21) gives you a child that keeps every value (request ID, user ID) but is never cancelled and has no deadline.
package mainimport ( "context" "fmt")type reqIDKey struct{}func main() { req, cancel := context.WithCancel(context.WithValue(context.Background(), reqIDKey{}, "req-7")) bg := context.WithoutCancel(req) // same values, no cancellation cancel() // the request is over fmt.Println("request ctx:", req.Err()) fmt.Println("background ctx:", bg.Err()) fmt.Println("still has request id:", bg.Value(reqIDKey{}))}What does context.AfterFunc do?
context.AfterFunc(ctx, f) (Go 1.21) runs f in its own goroutine once ctx is done. It returns a stop function: calling stop() before then cancels the plan, and it returns true if it stopped f from running. Use it to clean up, such as closing a connection, without writing a goroutine that waits on ctx.Done().
package mainimport ( "context" "fmt" "sync")func main() { ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup wg.Add(1) context.AfterFunc(ctx, func() { defer wg.Done() fmt.Println("ctx done, closing connection") }) ctx2, cancel2 := context.WithCancel(context.Background()) stop := context.AfterFunc(ctx2, func() { fmt.Println("never printed") }) fmt.Println("stopped before it ran:", stop()) cancel2() cancel() wg.Wait() // f runs in its own goroutine, so wait for it}The context rules
- First parameter, always, named `ctx`.
- Never store a Context in a struct. It belongs to one call, not to an object.
- Always call `cancel`.
defer cancel()on the line after you create it. - Cancelling does not stop anything by itself. It closes
ctx.Done(), and your code has to check that. Anything that blocks shouldselecton it. context.Valueis for request-scoped data crossing API boundaries, such as a request ID or a user ID. Not for dependencies or config. If the function needs it to work, it is a parameter.
Try it yourself
package mainimport ( "context" "fmt" "time")type ctxKey struct{} // unexported, so nobody else can collide with itfunc work(ctx context.Context, name string, d time.Duration) { select { case <-time.After(d): fmt.Println(name, "finished") case <-ctx.Done(): fmt.Println(name, "stopped:", ctx.Err()) // you must check this }}func main() { // 1. a deadline ctx, cancel := context.WithTimeout(context.Background(), 40*time.Millisecond) defer cancel() // always, even on the success path work(ctx, "slow job", 200*time.Millisecond) // 2. cancelling by hand ctx2, cancel2 := context.WithCancel(context.Background()) go func() { time.Sleep(20 * time.Millisecond) cancel2() }() work(ctx2, "cancelled job", time.Second) // 3. a request-scoped value ctx3 := context.WithValue(context.Background(), ctxKey{}, int64(42)) if id, ok := ctx3.Value(ctxKey{}).(int64); ok { fmt.Println("user id from context:", id) }}- First parameter, always, named
ctx. Never stored in a struct. - Always
defer cancel(), even on the success path. - Cancelling does not stop anything by itself. Your code must select on
ctx.Done(). context.Valueis for request-scoped identity, not for dependencies or config.
r.Context() so a disconnecting client stops your database query too.context.DeadlineExceeded.SleepCtx(ctx, d): wait for d, or stop early if ctx is done. Return nil if the time passed, or ctx.Err() if the context ended first. The hidden tests time you: a cancelled context must make a 2-second sleep return almost at once. Example: with a context that times out after 5ms, SleepCtx(ctx, 2*time.Second) returns context.DeadlineExceeded after about 5ms.Tab indents. Press Esc, then Tab, to leave the editor.
- 1Forget
defer cancel()and see whethergo vetcatches it. - 2Store a request id in a context with an unexported key type, and read it back.
- 3Write a loop that ignores
ctx.Done()and prove cancellation does nothing.