Irbisa · Cheatsheet
Context & Cancellation
The context tree, timeouts, deadlines, values, cancellation propagation
What is
context.Contextin Go, what is it used for, and which constructors does thecontextpackage provide?func handle(w http.ResponseWriter, r *http.Request) { // A child of the request context with a 2 s budget. ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) defer cancel() rows, err := db.QueryContext(ctx, "SELECT id FROM orders WHERE user_id = $1", userID(r))…What is the difference between
context.Background()andcontext.TODO()?What does this program print, and what does it show about how cancellation flows between parent and child contexts?
func main() { parent, cancelParent := context.WithCancel(context.Background()) child, cancelChild := context.WithCancel(parent) cancelChild() fmt.Println(parent.Err(), child.Err()) child2, cancelChild2 := context.WithTimeout(parent, time.Hour) defer cancelChild2() cancelParent() fmt.Println(parent.Err(), child2.Err())}What is the difference between
ctx.Done()andctx.Err(), and which errors canErr()return?func worker(ctx context.Context, jobs <-chan Job) error { for { select { case <-ctx.Done(): return ctx.Err() // context.Canceled or context.DeadlineExceeded case j := <-jobs:…Why must you always call the
cancelfunction returned bycontext.WithTimeoutorcontext.WithCancel?// wrong: vet (lostcancel) reports the discarded cancelfunc fetchBad(parent context.Context, id string) (*User, error) { ctx, _ := context.WithTimeout(parent, time.Second) return get(ctx, id)}…Why should
context.Contextbe passed as a function's first parameter rather than stored in a struct, and why should you never passnil?Does cancelling a context stop the goroutine that uses it? What must the goroutine itself do?
// wrong: time.Sleep ignores ctx, so the loop runs on after cancelfunc pollBad(ctx context.Context) { for { check() time.Sleep(10 * time.Second) }…When do you use
context.WithCancel,context.WithTimeoutandcontext.WithDeadline?Your repository wraps every query in
context.WithTimeout(ctx, 5*time.Second), yet in production queries abort after 300 ms. Where does 300 ms come from?func (r *Repo) Load(ctx context.Context, id int64) (*Row, error) { // A ceiling on what is left, not a grant of five seconds. ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if dl, ok := ctx.Deadline(); ok && time.Until(dl) < 20*time.Millisecond {…The cache lookup below should get 100 ms and the database up to 2 s, yet
h.db.Loadfails withcontext deadline exceededafter 100 ms. Why?func (h *Handler) Get(ctx context.Context, id string) (*Page, error) { ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() if p, err := h.cache.Get(ctx, id); err == nil { return p, nil } return h.db.Load(ctx, id) // the query needs about 300 ms}What does this program print, and how should you define context keys so that two packages cannot collide?
type key string func main() { ctx := context.WithValue(context.Background(), key("user"), "alice") ctx = context.WithValue(ctx, "user", "bob") fmt.Println(ctx.Value("user")) fmt.Println(ctx.Value(key("user"))) fmt.Println(ctx.Value("tenant"))}How should retries and error handling treat
context.Canceleddifferently fromcontext.DeadlineExceeded?func retry(ctx context.Context, attempts int, f func(context.Context) error) error { var err error for i := range attempts { actx, cancel := context.WithTimeout(ctx, time.Second) // per-attempt budget err = f(actx) cancel()…Find the bug: this queue worker's heap grows steadily for as long as it runs, although every job finishes in milliseconds.
func (w *Worker) Run(ctx context.Context) error { for { select { case <-ctx.Done(): return ctx.Err() case job := <-w.jobs: jobCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() if err := w.handle(jobCtx, job); err != nil { slog.ErrorContext(ctx, "job failed", "id", job.ID, "err", err) } } }}What happens to
r.Context()when the HTTP client disconnects, and how do you make the handler's outbound calls stop with it?var client = &http.Client{} // shared; no Client.Timeout, which would cut long streams func proxy(w http.ResponseWriter, r *http.Request) { // http.NewRequest would use context.Background() and ignore the disconnect. req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, upstream+r.URL.Path, nil) if err != nil {…Find the bug: under load, this service's goroutine count keeps climbing whenever the upstream gets slow.
func fetch(ctx context.Context, url string) ([]byte, error) { ctx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() ch := make(chan result) go func() { body, err := download(url) // legacy client: takes no context ch <- result{body, err} }() select { case r := <-ch: return r.body, r.err case <-ctx.Done(): return nil, ctx.Err() }}What does
context.WithoutCanceldo, and when do you need it?func handler(w http.ResponseWriter, r *http.Request) { // wrong: go audit.Write(r.Context(), ev) is cancelled when the handler returns // right: keep the values, drop the cancellation, add our own bound ctx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), 5*time.Second) ev := eventFrom(r) // read r now, not after the handler has returned…How is the
contextpackage implemented internally: how does cancellation reach the children, and why isValuelookup linear?Every failure in a fan-out reports just
context canceled. How do you make the cancellation carry its reason: which shard failed, which budget ran out?var ErrSearchBudget = errors.New("search budget exhausted") func search(parent context.Context, q string) ([]Hit, error) { ctx, cancel := context.WithTimeoutCause(parent, 200*time.Millisecond, ErrSearchBudget) defer cancel()…A third-party client has
Close()but takes nocontext.Context. How do you abort it on cancellation without parking a goroutine per call?// right: a registered callback instead of a waiting goroutinefunc call(ctx context.Context, c *vendor.Client) error { stop := context.AfterFunc(ctx, func() { c.Close() // runs in its own goroutine when ctx ends }) err := c.Run()…Does
srv.Shutdown(ctx)cancel the contexts of in-flight requests? How do you make long-running handlers stop during shutdown?func main() { sigCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() base, cancelBase := context.WithCancel(context.Background()) defer cancelBase()…Service A gives B 500 ms, but B calls C with a fixed 2 s timeout, and C burns CPU on requests nobody waits for. How do you propagate deadlines across service hops?
const hopHeadroom = 50 * time.Millisecond // Caller: put the remaining time on the wire, never an absolute timestamp.func forward(ctx context.Context, req *http.Request) (*http.Response, error) { if dl, ok := ctx.Deadline(); ok { budget := time.Until(dl) - hopHeadroom…