Context plumbing

March 19, 2025

Monday we looked at a very high-level view of what problems the context package is meant to solve. Namely, to propagate cancelation signales and/or request-scoped values through the call stack of an application.

Today we’ll take a brief look at how context “plumbing” works—and answer they why of all those mysterious context.Context function parameters you’ve probably seen, being passed around more than a virus in a daycare.

Overview

Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context. The chain of function calls between them must propagate the Context, optionally replacing it with a derived Context created using WithCancel, WithDeadline, WithTimeout, or WithValue.

From this paragraph, we start to see a picture of the “flow” of a Context value. Each incoming request is expected to create a context. This could be any kind of request, not just HTTP requests, though that is a common example.

And outgoing calls to other servers, should accept a Context. This means things like: Making a call to an external API or microservice, or making a call to a database.

By doing this, if the request is canceled (due to network connection failure, or the user hitting the Back button, or whatever), that cancelation can be propagated to the outgoing server call, and the call to the dependent microservice or database can be aborted, rather than sitting around consuming resources that aren’t ever needed.

Of course there are other cases when it’s appropriate to create or consume Context values, but we’ll stick with this simple explanation to start with. We’ll cover some of those later.

For now, the point is: How do we get that cancelation signal, or request-scoped values, from point A to point B?

We do that by passing a context.Context value. And by convention (which is frequently—and should be—enforced by a linter), that should always be the first argument to your function.

func Frobnicate(ctx context.Context, value int) error {

By convention, this variable is often called simply ctx as well. Though this is rarely enforced, I encourage you to follow that convention. Only stray from it in the rare situation that you have two or more context values in the same scope. (We’ll see some reasons for this later on.) In such a case, name your second (or third, or Nth) context with something descriptive. shutdownCtx or backgroundCtx, etc.

You’ll often see these ctx values being passed around seemingly willy-nilly, never being modified or read:

func Frobnicate(ctx context.Context, value int) error {
	partial, err := foo(ctx, value)
	if err != nil {
		return err
	}
	percent, err := bar(ctx, partial)
	if err != nil {
		return err
	}
	/* ... etc, etc */
}

There’s a very good chance you’ve been puzzled by this before. Hopefully now it’s beginning to make a bit of sense why these silent, ever-present values are being passed around so. They contain some virtually invisible signaling mechanism! And for that signaling mechanism to work, you need complete “context plumbing” from end-to-end.


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


Context cancelation

As already mentioned, the context.Context type serves two purposes: It manages cancelation signals, and it allows passing request-scoped data. In my opinion, these two purposes are distinct enough that they probably should be handled by two separate mechanisms. But alas, that’s not what we have. However, I think it’s still useful to think of the two uses as distinct. Most often (at least in a well-designed application), the primary purpose of a context is the propegation of cancelation signals.


Concurrent use of contexts

Overview … The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines. Contexts are built using layers. Every time you call one of the context.With* functions, the orginal context is wrapped in a new layer. This is the “magic” that makes contexts concurrency safe by design–they’re never mutated once they’re created. ctx := context.Background() // Create an original context ctx, cancel = context.


Context abuse

We’re nearly through the overview of the context package, when we come across this seemlingly straightforward sentence: Overview … Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions. But is that really so straight forward? Let’s consider some examples. Let’s first tackle what probably is straight-forward: For garden variety optional parameters, context is the wrong tool! Do this: // Connect connects to the service using the provided host and port, or the // default host and/or port if omitted.

Get daily content like this in your inbox!

Subscribe