Golang: Context

Go’s context package manages process lifecycles, handles timeouts & cancellations and propagates request-scoped metadata across API boundaries and concurrent goroutines. It acts as a command signal passed along a chain of workers (goroutines) that allows a parent process to tell everyone downstream to stop working immediately when they are no longer needed. Cancellation is cooperative: context only sends the signal, so your code has to check Done() and return.

Four Pillars

  • Done() - Returns a read-only channel (<-chan struct{}) that closes when work should stop.
    • Usage: listening in a select statement to halt loops or database queries.
  • Err() - Returns an error explaining why the channel closed: context.Canceled or context.DeadlineExceeded.
    • Usage: checking why an operation failed or logging the termination reason.
  • Deadline() - Returns the exact time (time.Time) this context will be automatically canceled, plus an ok bool that is false if there is no deadline.
    • Usage: checking if there is enough time left to begin an expensive step.
  • Value() - Fetches an immutable, request-scoped value associated with a specific key. It returns any, so callers type-assert the result.
    • Usage: reading request data like a trace ID or the authenticated user.

Creating Contexts

  • context.Background() - The default empty root context. Use this at the highest level of your application or the entry point of a request handler.
  • context.TODO() - Placeholder context.
  • context.WithCancel(Parent) - Returns a copy of the parent along with a cancel() function. Invoking cancel() manually triggers the closure of the child’s Done() channel.
  • context.WithTimeout(Parent, Duration) - Automatically cancels the context after a set timeframe passes.
  • context.WithDeadline(Parent, Time) - Automatically cancels the context at a specific time.Time.
  • context.WithValue(Parent, Key, Value) - Attaches custom data to the context.

Example

work runs a step every 300ms until its context times out after one second.

func work(ctx context.Context) error {
	for step := 1; ; step++ {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-time.After(300 * time.Millisecond):
			fmt.Println("finished step", step)
		}
	}
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	if err := work(ctx); err != nil {
		fmt.Println(err) // after 3 steps: context deadline exceeded
	}
}

Golden Rules

  • Pass it First - Always make ctx context.Context the very first parameter of a function.
  • Never Store in Structs - Pass context explicitly through call stacks rather than keeping it inside an object instance.
  • Always Defer Cancel - When using WithCancel, WithTimeout or WithDeadline, always immediately run defer cancel() to release the context’s resources (its timer and its link to the parent) as soon as the function returns.
  • Keep Values Lightweight - Don’t use WithValue to pass database connections or optional parameters. Use it strictly for request-scoped variables like user tracking or trace IDs, and make keys an unexported custom type so they don’t collide with keys from other packages.
Coding Experience Uses