Lesson 16 · 16 of 20
context: timeouts and cancellation · Go
Put a time limit on an operation and cancel it with context in Go: WithTimeout, WithCancel, ctx.Done() and ctx.Err() with a slow supplier.

A `context.Context` travels from function to function carrying one instruction: stop what you are doing. By convention it is the first parameter and is called ctx, as in fetchQuote(ctx, supplier, delay). Go's networking and database libraries accept one in almost every function.
fetchQuote simulates a supplier that takes delay to quote a price. Its select waits for two things: that time passing, or `ctx.Done()` being closed, a channel that closes when the context expires or is cancelled. Whichever comes first wins. `ctx.Err()` gives the reason, and it is wrapped with %w so it is not lost.
`context.WithTimeout` creates a context that expires on its own, here after 100 ms. FastParts answers in 10 ms and gives its price; SlowSupply would take a second, so the deadline wins and the error is context deadline exceeded. errors.Is(err, context.DeadlineExceeded) recognises it even though it is wrapped.
WithTimeout also returns a cancel function, and you must always call it, even if the deadline never arrives, to release its resources. Usually you write defer cancel(); inside a loop you call it at the end of each iteration, because a defer would wait until main returns.
`context.WithCancel` does not expire on its own: it is cancelled by calling cancel(), for instance when the customer closes the page. Here it is cancelled before asking for the price, so ctx.Done() is already closed and the error is context canceled. The times (10, 100 and 1000 ms) are so far apart that the output is always the same.
Code · main.go
package mainimport ( "context" "errors" "fmt" "time")// fetchQuote simulates a supplier that answers after delay, unless ctx ends first.func fetchQuote(ctx context.Context, supplier string, delay time.Duration) (string, error) { select { case <-time.After(delay): return supplier + " quotes 42.50 EUR", nil case <-ctx.Done(): // closed on timeout or on cancel return "", fmt.Errorf("%s: %w", supplier, ctx.Err()) }}func main() { delays := map[string]time.Duration{"FastParts": 10 * time.Millisecond, "SlowSupply": time.Second} for _, supplier := range []string{"FastParts", "SlowSupply"} { ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) quote, err := fetchQuote(ctx, supplier, delays[supplier]) cancel() // always release the context, even if it was not needed fmt.Printf("%-28q err=%v timeout=%t\n", quote, err, errors.Is(err, context.DeadlineExceeded)) } ctx, cancel := context.WithCancel(context.Background()) cancel() // e.g. the customer closed the page before we asked _, err := fetchQuote(ctx, "FastParts", 10*time.Millisecond) fmt.Println("err:", err, "| canceled:", errors.Is(err, context.Canceled))}Output · go run main.go
"FastParts quotes 42.50 EUR" err=<nil> timeout=false "" err=SlowSupply: context deadline exceeded timeout=true err: FastParts: context canceled | canceled: true
Key points
ctxalways goes first in the parameter list.ctx.Done()closes when the deadline passes or on cancel.ctx.Err()says why:DeadlineExceededorCanceled.- Always call
cancel(), usually withdefer. errors.Isrecognises the reason even when the error is wrapped.
Exercise
Ask both suppliers at once, each in its own goroutine and with the same 100 ms context, store each result in its slot of a slice and print them in order once both are done.