Concept · Go

Context and cancellation

One value that carries a deadline and a cancel signal through every call a request makes.

3 mincontext.Contextctx.DoneWithTimeoutWithCancel

A request handler might query a database, call two services and start a goroutine. If the client disconnects or the deadline passes, all of that work should stop. Go's answer is context.Context: a value passed as the first argument of every function that does I/O. It carries:

  • a cancel signal, through ctx.Done(), a channel that closes when the work should stop;
  • a deadline, which fires that signal automatically;
  • the reason, from ctx.Err(), which is context.Canceled or context.DeadlineExceeded;
  • a few request-scoped values, like a trace ID. Use these sparingly.

Deriving and passing

Contexts form a tree. A child gets cancelled when its parent does, and it can only tighten the deadline, never extend it.

go
func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) // cancelled if client leaves, or after 2s
	defer cancel()                                                // always: releases the timer

	order, err := h.store.GetOrder(ctx, id)         // database/sql and pgx honour ctx
	if errors.Is(err, context.DeadlineExceeded) { /* 504 */ }
	// ...
}

The standard library and good clients (net/http, database/sql, pgx, gRPC) watch the context and abort when it's done. Your own long loops and goroutines have to check it themselves:

go
for job := range jobs {
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}
	process(job)
}

Rules

  • Pass it along explicitly; don't store it in a struct. Whatever a function does on the request's behalf should use the request's context.
  • Always defer cancel(). Skipping it leaks the context's resources until the parent is cancelled.
  • Never pass nil. Use context.Background() at the top of main or a background job, and context.TODO() while you're refactoring.
  • A goroutine that ignores its context leaks. If the handler returns and nobody reads the goroutine's channel or cancels its work, it lives forever. Give every goroutine a way to stop.
  • Use context.WithoutCancel(ctx) (Go 1.21+) for work that must finish even if the request is cancelled, such as writing an audit record. It keeps the values but drops the cancellation.

This is how Go implements deadline propagation, and the same signal drives graceful shutdown.

Learn it in depth