I am releasing a series of articles to dive deep into the internals of a few packages.
If you are interested, I’ll post their release on Twitter and LinkedIn .
Context type(s)
This article deeps dive into the context types implementation provided by the standard library as part of the context package itself.
Disclaimer: This article will not describe how to use the package or what are the best practices. It aims only to describe the internals of the package.
The context package allows Go programs to carry values across API boundaries,
transmit deadlines and cancellation signals through the Context
interface.
package context
// ...
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L62
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
The package doesn’t offer any exported type implementing this interface, in fact, to allow engineers to perform the expected behaviors the context package holds four unexported types provided through functions.
emptyCtx
The most used function from the context package is named Background
,
which returns a value implementing the context.Context
interface.
Next to Backgroud
, there’s another function named TODO
that behave in the same way,
it simply offers a different semantic as it is recommended to use that
when it is unclear which “context type” to use or when the context is not passed from a caller.
The implementation of the two functions looks like this:
package context
// ...
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L208
func Background() Context {
return background
}
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L216
func TODO() Context {
return todo
}
Both background
and todo
are package-level instantiated variables
of an unexported type named emptyCtx
.
Just by itself, the emptyCtx
is a pretty useless implementation,
since it is a context that holds no value nor implements any logic.
package context
// ...
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L199
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key any) any {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
The emptyCtx
is the foundation context that enables any other type to be created,
since it is the only type that does not require a parent context to be instantiated.
valueCtx
As stated before, a feature of the context package is the ability to carry values across API boundaries.
This is achieved utilizing the valueCtx
type, returned by the WithValue
function.
package context
//...
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L523
func WithValue(parent Context, key, val any) Context {
//...
return &valueCtx{parent, key, val}
}
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L538
type valueCtx struct {
Context
key, val any
}
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L562
func (c *valueCtx) Value(key any) any {
// ...
}
The valueCtx
type defines the Value
method,
and leverage the embedded field passed as parent
in the WithValue
function to implement the Context
interface.
The Value
method allows retrieving a value set during its creation (see WithValue
)
if a given input key
is equal to the homonym type field.
package context
// ...
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L562
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
func value(c Context, key any) any {
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
// ...
case *emptyCtx:
return nil
default:
return c.Value(key)
}
}
}
As you can see, the value
function traverses all the parent contexts
until it finds one matching the given key
or returns nil if not.
The implementation details show that valueCtx
acts like a node of a singly linked list,
the linked list (spoiler: not always singly!) is the data structure used to read/write data by the context package.
cancelCtx
Besides carrying values across API boundaries, the context package is used for deadlines and cancellation signals.
The cancellation behaviors are available after using the WithCancel
method.
The code is implemented as such:
package context
// ...
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L342
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
// ...
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L232
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
// ...
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L249
func propagateCancel(parent Context, child canceler) {
//...
}
The function seems to behave similarly to the WithValue
one at first glance,
it creates an instance of a different type tho, named cancelCtx
,
setting its parent as an embedded field and returning the variable alongside a c.cancel
function.
Here is where things start to get interesting.
The cancelCtx
doesn’t behave in the same way as valueCtx
due to the data structure used,
it is implemented as a doubly linked list node and not a singly linked list,
the link is created in the propagateCancel
function (see the comments I added in the below snippet).
package context
// ...
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L249
func propagateCancel(parent Context, child canceler) {
// ... logic handling already terminated contexts.
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L2649
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
// ...
// Here the child is stored in the parent, initialising a map if this is the first one.
// The map is used in favor of a slice for performance purposes mainly as for searching a single child will have an impact of O(1).
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
p.mu.Unlock()
}
// ...
}
The purpose of using this data structure is to ensure both child and parent are informed of a context declared
done (more precisely, “canceled”) when calling the CancelFunc
returned by the WithCancel
function.
Behind the scene,
when invoking CancelFunc
,
the cancel
unexported method of the cancelCtx
does the heavy lifting (see the comments I added in the below snippet).
package context
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L232
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
// ...
return &c, func() { c.cancel(true, Canceled) }
}
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L397
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
// ...
// Note: this code section is simplified from the original one for reading purposes.
// TLDR:
// Sets the reason why the context was canceled.
// If the error is set and the context is marked again as canceled it will not take any action,
// in this case, is set as `context.Canceled`
// which you can see here https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L157
c.err = err
// ...
// If the context has a channel in the `done` field
// it means that a goroutine _may_ be interested in knowing
// the context is now done, so the channel is closed.
d, _ := c.done.Load().(chan struct{})
close(d)
// ...
// Cancels all the registered children.
for child := range c.children {
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
// ...
if removeFromParent {
removeChild(c.Context, c)
}
}
The cancelCtx
is also the only type
implementing behaviors for the Done
method,
used by goroutines to determine if an operation still has time to perform the tasks (see the comments I added in the below snippet).
package context
// ...
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L358
func (c *cancelCtx) Done() <-chan struct{} {
// If there is already a channel created then returns it
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
// Otherwise, creates a new one and returns it
c.mu.Lock()
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
timerCtx
A type with similar behavior and mechanisms to cancelCtx
,
is the last one implemented in the standard library’s context package: timerCtx
.
They are so similar that the cancelCtx
is used as an embedded field of the timerCtx
.
package context
// ...
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L465
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
The timerCtx
type defines the Deadline
method,
that returns a time.Time
when the context can be considered done.
The timerCtx
can be created by two functions from the context package, WithTimeout
and WithDeadline
.
The two have similar behavior since the WithTimeout
is a simple wrapper for WithDeadline
,
the main difference is found in the type used as input to pass down to the timerCtx
,
The logic implemented by WithDeadline
is the most complex in the package, in my opinion,
because it needs to handle a few edge cases,
but that said, it is still pretty straightforward (see the comments I added in the below snippet).
package context
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L506
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
// ...
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L434
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
// If the parent deadline is prior to the new one we're creating
// it returns a cancelCtx since we don't need the overhead of a `timerCtx`.
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
return WithCancel(parent)
}
// Creates a timerCtx.
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// Ensures the timerCtx will be canceled when the parent is marked as such.
propagateCancel(parent, c)
// If the deadline is already passed (maybe you passed a time.Time in the past?)
// it immediately cancels timerCtx and returns a CancelFunc that will do nothing when invoked.
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
// Creates a timer that will mark the context done as `DeadlineExceeded` when the deadline is reached.
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
// Returns the timerCtx and a CancelFunc that can be called before the timer is triggered.
return c, func() { c.cancel(true, Canceled) }
}
When the CancelFunc
function returned by WithTimeout
or WithDeadline
is invoked,
the inner cancel
method of timerCtx
propagates the call to the cancelCtx
hold as an embedded field,
removing itself from its parent cancelCtx
(if necessary).
On top of that, unlike cancelCtx
, the timeCtx
needs to stop its inner timer as well.
package context
// https://github.com/golang/go/blob/go1.19.5/src/context/context.go#L482
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
if removeFromParent {
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
Conclusion
The types used in the standard library as context.Context implementations are now known and broken down into simpler pieces.
In the following weeks, I’ll release a second part of the article to show as animated slides how different context types work and how their mechanisms kick in real-life examples.
Feel free to send me a DM on Twitter, LinkedIn or add a comment in the section below!