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.