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. And that’s what we’re talking about today…
Overview
…
A Context may be canceled to indicate that work done on its behalf should stop. A Context with a deadline is canceled after the deadline passes. When a Context is canceled, all Contexts derived from it are also canceled.
So when a context is canceled, that’s an indication that work should be canceled, as it will no longer be of any use to the caller. That cancelation can be triggered in one of two ways:
- After a timeout.
- When the context is explicitly canceled.
The WithCancel, WithDeadline, and WithTimeout functions take a Context (the parent) and return a derived Context (the child) and a CancelFunc. Calling the CancelFunc directly cancels the child and its children, removes the parent’s reference to the child, and stops any associated timers. Failing to call the CancelFunc leaks the child and its children until the parent is canceled. The go vet tool checks that CancelFuncs are used on all control-flow paths.
The context
gives us three basic ways to construct cancelable contexts (there are a few derivatives we’ll discuss later.):
WithDeadline
andWithTimeout
allow specifying a time-based cancelation.WithCancel
returns aCancelFunc
that can be used to explicitly cancel a function.
I’ll show an examples, but we’ll save a detailed discussion for each functoin until later.
func doSomething(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 10 * time.Second)
defer cancel()
if err := doSomethingElse(ctx); err != nil {
return err
}
if err := doYetMoreThings(ctx); err != nil {
return err
}
return nil
}
In tihs example, we take an existing context value, passed into our doSomething
function, then from that derive a new context from it with a 10-second timeout, and a CancelFunc
function (cancel
). Then we pass that new context to doSomethingElse()
.
In this configuration, both doSomethingElse
and doYetMoreThings
should stop processing, and return early, if any of the following conditions are met:
- The original context (the one passed to
doSomething
) is canceled, - 10 seconds pass,
- the
cancel()
function is called explicitly.
If doSomethingElse
spends 8 seconds doing work, then doYetMoreThings
will have only 2 seconds remaining to do its work before the context will be canceled.
Note that in this example, the explicit call to cancel()
is only invoked when doSomething()
returns (by way of calling it with the defer
keyword). This means that doSomethingElse
and doYetMoreThings
will never be canceled for this reason. But even in such cases, you should always call the returned CancelFunc
function, to clean up any resources used by the context.