gctx

Implementation of a goroutine context (gctx
).
TL;DR
Set a goroutine context with gctx.Set(ctx)
and retrieve it from anywhere in
the same or a child goroutine with gctx.Get()
.
A gctx is a context.Context
that is neither global, nor in the local function
scope, but a property of a specific goroutine. It can only be created and
retrieved by code being executed within the according goroutine. The gctx can be
set with gctx.Set()
:
ctx := context.WithValue(context.Background(), myCtxVal{}, "foobar")
go func() {
gctx.Set(ctx)
...
/* some code that eventually calls doSomething() */
...
}()
Then, from any function called within the same or a child goroutine this gctx
can be retrieved with gctx.Get()
:
func doSomething() {
...
ctx := gctx.Get()
v := ctx.Value(myCtxVal{})
log.Printf("ctx: %v", v) // prints "ctx: foobar"
...
}
Why
The Go language decided for good reasons that goroutines are anonymous by
design. So there is no ID or any
goroutine-local storage exposed. (Well, almost, see below.) It is advised to
always use a context.Context
as the first parameter in all functions that
require knowledge about a specific context.
However, there are also good reasons to have such a goroutine specific context
generally available. The most common reason is logging:
- There are libraries that don't support a context parameter (yet). When such a
library is producing logs, one might want to annotate the log message with the
context in which the library call has been made. But without the possibility
to "tunnel" a context through the library to the log callback, that is not
possible.
- Debug logs or tracing appear anywhere in your code. Annotating these logs
with the current context would require to add a context parameter to every
single function, which is simply unnecessary boilerplate and in most cases
not feasible.
gctx
allows to solve these situations by attaching a context to the current
goroutine, which can be retrieved from any code, that is running in the same
goroutine or a child, with a simple gctx.Get()
call. It is effectively a
goroutine-local storage, and could also be used to identify a goroutine, which
is not recommended. Therefore it should only be used, if there is really no
other possibility available.
How
In fact, goroutines do have an
ID,
but it is hidden and not accessible with any exported API. That's why there have
been a couple of attempts to circumvent that, by implementing a goroutine-local
storage in order to solve above problems, like for example:
But all these packages are pretty inefficient and/or are highly dependent on
implementation details.
Fortunately, there is another goroutine property that is - at least partly -
exported: profiling labels. They can be
stored with
runtime/pprof.SetGoroutineLabels()
in the goroutine-local storage and are automatically inherited by child
goroutines. But again, there is no exported API that allows to read these label
from the goroutine local storage.
So, in order to make (ab)use of these labels for a gctx
, it is required to do
one little runtime hack: the internal functions
setProfLabel()
and
getProfLabel()
must be made accessible with a //go:linkname
instruction. (Of course this is
also an implementation detail, that could break anytime. But since it is part of
an exported functionality, it can't just disappear, and a fix should always be
easily possible.)
These functions are managing a pointer to a labelMap
(which is a
map[string]string
) in the goroutine-local storage. gctx
extends this type to
piggyback a context.Context
on it:
struct {
labelMap
context.Context
}
It stores pointers to objects of this extended yet compatible type instead of
the original labelMap
. A special label in the labelMap
is set that
indicates, that a context is available. That way the attached context is - like
the labels - automatically memory managed and inherited by child goroutines.
Usage
See
documentation
and examples.