Skip to main content
Context package

Context package

8 minutes read

Filed underGo Programming Languageon

Learn how Go's context package enables cancellation, deadlines, and request-scoped values across API boundaries and concurrent operations.

Why context matters

In most real applications, operations don't run in isolation. An HTTP handler spawns a database query, which might trigger a cache lookup, which calls an external API. All these operations are chained — and they all share a common fate: if the original request is cancelled (the user closes the browser, for example), every downstream operation should stop too.

That's the problem the context package solves. It provides a standard mechanism to carry cancellation signals, deadlines, and request-scoped values across API boundaries and between goroutines.

Without context, you would need to invent your own cancellation channels, pass them everywhere, and coordinate teardown manually. With context, Go standardizes that contract under a single, well-understood interface.

The Context interface

The context package defines a single interface:

Each method represents one capability:

MethodWhat it tells you
Deadline()Returns the time when the context will be cancelled, if one was set
Done()Returns a channel that is closed when the context is cancelled or times out
Err()Returns context.Canceled or context.DeadlineExceeded after Done is closed
Value(key)Returns the value associated with a key stored in the context

You rarely implement this interface yourself. Instead, you use the functions the package provides to create and derive contexts.

Root contexts

Every context tree starts from a root. The package provides two constructors for this:

context.Background() is the conventional starting point. You call it at the top of your program — in main, in a server's request handler setup, or at the root of a test — and then derive child contexts from it.

context.TODO() signals that you haven't decided what context to use yet. It behaves identically to Background() at runtime, but it serves as a marker that the code needs revisiting. It's a useful placeholder when you're adding context support to existing code incrementally.

General rules

The context package comes with a few conventions that the Go community treats as firm rules.

Context should be created at the top of the call. Don't create a context deep inside a function and let it escape upward. The context's lifecycle should reflect the lifecycle of the operation it governs, which starts at the entry point.

Context is the first parameter of a function. By convention, if a function accepts a context, it is always the first parameter and always named ctx:

This consistency makes it immediately clear at a glance that a function participates in the context system.

Never store context in a struct field. Contexts are request-scoped. Storing them in a struct implies they could outlive the request or be reused across requests, both of which are bugs waiting to happen. Pass context as an explicit parameter every time.

Never mutate a context. Each derivation function (like WithCancel or WithValue) returns a new context. The original is never modified. This immutability makes it safe to pass contexts across goroutines without synchronization.

Deriving contexts

You don't use root contexts directly for most operations. Instead, you derive them — adding cancellation, a deadline, or a value — and pass the derived context downstream. Each derived context is a child of its parent; cancelling a parent cancels all its children automatically.

WithCancel

context.WithCancel returns a child context and a cancel function. Calling cancel closes the context's Done channel, signalling all receivers that they should stop work:

The defer cancel() pattern is critical. Failing to call cancel leaks resources — the runtime holds onto internal state associated with the context until it's cancelled or its parent is cancelled. Always defer cancel() immediately after calling WithCancel.

WithTimeout and WithDeadline

context.WithTimeout is the most common way to enforce a maximum duration on an operation:

context.WithDeadline does the same thing, but you specify an absolute point in time instead of a duration:

Both functions return the same kind of context — WithTimeout is just a shorthand for WithDeadline(parent, time.Now().Add(timeout)).

Always defer cancel

Both WithTimeout and WithDeadline also return a cancel function, even though the context will cancel itself automatically when the deadline is reached. You should still defer cancel() — it frees resources earlier if the operation completes before the deadline fires.

When the deadline passes, ctx.Done() is closed and ctx.Err() returns context.DeadlineExceeded. Well-behaved libraries (like the standard net/http client) check the context and return promptly when this happens.

WithValue

context.WithValue lets you attach a value to a context that can be retrieved anywhere downstream:

A few important details about WithValue:

  • The key must be a comparable type. Use a private named type (like contextKey above) rather than a bare string to avoid collisions with other packages that might use the same string literal.
  • Values stored in context should be request-scoped — things like request IDs, authentication tokens, or tracing metadata. Don't use context as a way to pass optional function parameters.
  • Value traverses the context chain from child to parent, so a value set in a parent context is visible to all its descendants.

Context is not a parameter substitute

It can be tempting to stuff function inputs into context to avoid changing signatures. Resist the urge. Context values are untyped, invisible to the compiler, and impossible to discover without reading source code. Use context for cross-cutting concerns — observability, cancellation, identity — not for domain logic.

Context in practice

HTTP request handling

Every *http.Request carries a context accessible via r.Context(). When the client disconnects, Go's HTTP server cancels that context automatically. This means any database query or downstream call made with that context will also be cancelled — no extra wiring needed:

Database queries

Most database drivers for Go accept a context. Passing the request context to your queries means they're automatically cancelled when the caller goes away:

Long-running goroutines

For background workers or other long-running goroutines, context provides a clean shutdown signal:

The select statement checks both the job channel and ctx.Done() on every iteration. When the context is cancelled — whether by a timeout, a manual cancel() call, or a parent cancellation — the goroutine exits cleanly without polling or busy-waiting.

Context propagation is one of those patterns that feels like overhead at first but pays dividends as your system grows. Once you have cancellation flowing from the outermost request all the way into your goroutines and database calls, you get coordinated, leak-free teardown essentially for free.