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!