Context is a very important component, if you write Golang, you can see it every where, within a Request, DB query, outgoing call, Goroutine .etc. Almost every where when there’re side effects. We can use Context to cancel operation, cancel operation before deadline, cancel operation with timeout, or carry some request scoped data, like current user, tracing data .etc.

Have you ever been inquisitive when using Context? I have.

What’s the difference between TODO() and Background()?

According to official docs

https://golang.org/pkg/context/#Background

Background returns a non-nil, empty Context. It is never canceled, has no values, and has no deadline. It is typically used by the main function, initialization, and tests, and as the top-level Context for incoming requests.

https://golang.org/pkg/context/#TODO

TODO returns a non-nil, empty Context. Code should use context.TODO when it’s unclear which Context to use or it is not yet available (because the surrounding function has not yet been extended to accept a Context parameter).

They both claim they return empty Context, but are they any different from each other? Let’s check the source code.

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
	return background
}

// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
	return todo
}

Okay, they’re no different from implementation, only the context of where they should be used.

Why Context can be passed along?

Take a look at following example

ctx, cancel := context.WithDeadline(parent, time.Now().Add(100*time.Millisecond))
defer cancel()

db.QueryContext(ctx, stmt, args...)

We create new Context with timeout and pass it to QueryContext, when timeout, it cancels the operation. Let’s dissect WithDeadline.

// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     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
}

From cancelCtx we know it persists the children in children map.

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
  ...
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
	propagateCancel(parent, c)
  ...
}

func propagateCancel(parent Context, child canceler) {
  ...
	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			// parent has already been canceled
			child.cancel(false, p.err)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
    ...
	}

}

p, ok := parentCancelCtx(parent) returns the cancelCtx of parent context. The next 7th line creates a new children if children is empty, then insert the child into the children map.

What’s the rationale behind timeout/deadline?

First, WithTimeout returns WithDeadline, more like a shortcut for easy use.

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
  ...
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
  ...
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}

Easy to notice it utilize time.AfterFunc under the hood, after certain time duration, it triggers the execution of cancellation.

How operation is canceled?

Let’s revisit cancel(removeFromParent bool, err error).

// closedchan is a reusable closed channel.
var closedchan = make(chan struct{})

func init() {
	close(closedchan)
}

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
  ...
	c.err = err
	if c.done == nil {
		c.done = closedchan
	} else {
		close(c.done)
	}
  ...
}

When we call defer cancel(), it actually calls above function. In the function, it just closes the done channel or assign a closed one to it. Then we know it’s time to stop the operation. Excerpt a few lines from https://golang.org/src/database/sql/sql.go.

// Runs in a separate goroutine, opens new connections when requested.
func (db *DB) connectionOpener(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			return
		case <-db.openerCh:
			db.openNewConnection(ctx)
		}
	}
}

At line 4, if operation times out, c.done is closed, then in subsequent calls, ctx.Done is unblocked, then that corresponding branch will be executed. In this case, it directly exit the function.

Will it cancel all children operations if I cancel parent context?

Yes, from above code snippet, you probably have the sense of it. Let’s read rest of cancel(removeFromParent bool, err error).

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
  ...
	if c.done == nil {
		c.done = closedchan
	} else {
		close(c.done)
	}
	for child := range c.children {
		// NOTE: acquiring the child's lock while holding parent's lock.
		child.cancel(false, err)
	}
	c.children = nil
	c.mu.Unlock()

	if removeFromParent {
		removeChild(c.Context, c)
	}
}

After current context is canceled, it’ll keep asking its children to cancel the operations. At 6th line, it iterates all children and send message to each one. Each of the child will do same as current context does.

How is scoped data stored?

Key/value can be set by WithValue(parent Context, key, val interface{}) Context.

func WithValue(parent Context, key, val interface{}) Context {
  ...
	return &valueCtx{parent, key, val}
}

type valueCtx struct {
	Context
	key, val interface{}
}

func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}

OK, it’s a linked list (reverse linked list) you’ll see. Current node points to its parent. Actually all types of contexts, the underlying data structure is a reverse linked list, like WithDeadline, WithTimeout, WithValue.

Thoughts?

  • Why use linked list?
  • What’s the downside of using linked list?
  • Why Context is used to manage both life cycle and scoped data? Is this a good idea?

Conclusion

Context in Golang has three kinds of internal data type emptyCtx, cancelCtx, valueCtx, they serve different purposes and they all implement interface Context. cancelCtx derives new context and stores them as children, use time.AfterFunc to cancel an operation. Whereas, valueCtx derives new context and stores key/value pair on the new context object.