This is an experience report about the use of, and difficulties with, the context.Context
facility in Go.
Many authors, including myself, have written about the use of, misuse of, and how they would change, context.Context
in a future iteration of Go. While opinions differs on many aspects of context.Context
, one thing is clear–there is almost unanimous agreement that the Context.WithValue
method on the context.Context
interface is orthogonal to the type’s role as a mechanism to control the lifetime of request scoped resources.
Many proposals have emerged to address this apparent overloading of context.Context
with a copy on write bag of values. Most approximate thread local storage so are unlikely to be accepted on ideological grounds.
This post explores the relationship between context.Context
and lifecycle management and asks the question, are attempts to fix Context.WithValue
solving the wrong problem?
Context is a request scoped paradigm
The documentation for the context
package strongly recommends that context.Context
is only for request scoped values:
Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx:
func DoSomething(ctx context.Context, arg Arg) error { // ... use ctx ... }
Specifically context.Context
values should only live in function arguments, never stored in a field or global. This makes context.Context
applicable only to the lifetime of resources in a request’s scope. Given Go’s lineage on the server, this is a compelling use case. However, there exist other use cases for cancellation where the lifetime of the resource extends beyond a single request. For example, a background goroutine as part of an agent or pipeline.
Context as a hook for cancellation
The stated goal of the context
package is:
Package context defines the Context type, which carries deadlines, cancelation signals, and other request-scoped values across API boundaries and between processes.
Which sounds great, but belies its catch-all nature. context.Context
is used in three independent, yet sometimes conflated, scenarios:
- Cancellation via
context.WithCancel
. - Timeout via
context.WithDeadline
. - A bag of values via
context.WithValue
.
At any point, a context.Context
value can represent any one, or all three of these independent concerns. However, context.Context
‘s most important facility, broadcasting a cancellation signal, is incomplete as there is no way to wait for the signal to be acknowledged.
Looking to the past
As this is an experience report, it would be germane to highlight some actual experience. In 2012 Gustavo Niemeyer wrote a package for goroutine lifecycle management called tomb
which is used by Juju for the management of the worker goroutines within the various agents in the Juju system.
tomb.Tomb
s are concerned only with lifecycle management. Importantly, this is a generic notion of a lifecycle, not tied exclusively to a request, or a goroutine. The scope of the resource’s lifetime is defined simply by holding a reference to the tomb value.
A tomb.Tomb
value has three properties:
- The ability to signal the owner of the tomb to shut down.
- The ability to wait until that signal has been acknowledged.
- A way to capture a final
error
value.
However, tomb.Tomb
s have one drawback, they cannot be shared across multiple goroutines. Consider this prototypical network server where a tomb.Tomb
cannot replace the use of sync.WaitGroup
.
func serve(l net.Listener) error { var wg sync.WaitGroup var conn net.Conn var err error for { conn, err = l.Accept() if err != nil { break } wg.Add(1) go func(c net.Conn) { defer wg.Done() handle(c) }(conn) } wg.Wait() return err }
To be fair, context.Context
cannot do this either as it provides no built in mechanism to acknowledge cancellation. What is needed is a form of sync.WaitGroup
that allows cancellation, as well as waiting for its participants to call wg.Done
.
Context should become, well, just context
The purpose of the context.Context
type is in it’s name:
context /kɒntɛkst/ noun
The circumstances that form the setting for an event, statement, or idea, and in terms of which it can be fully understood.
I propose context.Context
becomes just that; a request scoped association list of copy on write values.
Decoupling lifetime management from context.Context
as a store of request scoped values will hopefully highlight that request context and lifecycle management are orthogonal concerns.
Best of all, we don’t need to wait til Go 2.0 to explore these ideas like Gustavo’s tomb
package.