The context package in Go

Who am I?

Damiano Petrungaro

Damiano Petrungaro

I know... I look younger and thinner in this pic, but HEY DO NOT JUDGE ME!

Italy

source: www.vidiani.com

source: www.romaest.org

Me everyday:

Context

Why should I use it?

How to use it?

What are the best practices?

How it is implemented?

Context

Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.

Using the "context" pkg

package context


type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

Using the "context" pkg

package context

func Background
func TODO
func WithValue
func WithTimeout
func WithDeadline
func WithCancel

Using the "context" pkg

package context

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

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}

the foundations

The emptyCtx

package context

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 interface{}) interface{} {
	return nil
}

the foundations

WithValue

...and other request-scoped values across API boundaries and between processes.

WithValue

package context

func WithValue(parent Context, key, val interface{}) Context {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

The valueCtx

package context

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)
}

A linked list node

package main

import "context"

func main() {
	ctx := context.Background()

	ctx = context.WithValue(ctx, "name", "Damiano")

	ctx = context.WithValue(ctx, "surname", "Petrungaro")

	ctx.Value("surname")

	ctx.Value("name")

	ctx.Value("none")
}

(name = Damiano)

(surname = Petrungaro)

Demo

Cancellation

Package context defines the Context type, which carries deadlines, cancellation signals...

WithCancel

package context

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

cancelCtx

package context

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
}

func (c *cancelCtx) Value(key interface{}) interface{} {
	if key == &cancelCtxKey {
		return c
	}
	return c.Context.Value(key)
}

func (c *cancelCtx) Done() <-chan struct{} {
	c.mu.Lock()
	if c.done == nil {
		c.done = make(chan struct{})
	}
	d := c.done
	c.mu.Unlock()
	return d
}

func (c *cancelCtx) Err() error {
	c.mu.Lock()
	err := c.err
	c.mu.Unlock()
	return err
}

A bidirected graph node

propagateCancel

package context

func propagateCancel(parent Context, child canceler) {
	done := parent.Done()
	if done == nil {
		return // parent is never canceled
	}

	select {
	case <-done:
		// parent is already canceled
		child.cancel(false, parent.Err())
		return
	default:
	}

	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 {
		atomic.AddInt32(&goroutines, +1)
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}

cancel

package context

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return // already canceled
	}
	c.err = err
	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)
	}
}
package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

const waitFor = ... to be defined

func main() {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	wg := &sync.WaitGroup{}

	for i := 0; i < 3; i++ {
		wg.Add(1)
		go schedule(ctx, wg, i)
	}

	time.Sleep(waitFor)
	cancel()
	wg.Wait()
	fmt.Println("done")
}

func schedule(ctx context.Context, wg *sync.WaitGroup, i int) {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()
	defer wg.Done()
	work(ctx, fmt.Sprintf("worker %d", i))
}

func work(ctx context.Context, prefix string) {
	ctx = context.WithValue(ctx, "name", "Damiano")
	ctx = context.WithValue(ctx, "surname", "Petrungaro")

	fmt.Printf("%s: started\n", prefix)
	select {
	case <-time.Tick(3 * time.Second):
		fmt.Printf("%s: finished\n", prefix)
	case <-ctx.Done():
		fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
	}
}

Demo

Demo

(name = Damiano)

(surname = Petrungaro)

(name = Damiano)

(surname = Petrungaro)

(name = Damiano)

(surname = Petrungaro)

main

schedule

work

Demo

(name = Damiano)

(surname = Petrungaro)

(name = Damiano)

(surname = Petrungaro)

(name = Damiano)

(surname = Petrungaro)

main

schedule

work

Demo

(name = Damiano)

(surname = Petrungaro)

(name = Damiano)

(surname = Petrungaro)

(name = Damiano)

(surname = Petrungaro)

main

schedule

work

WithTimeout

package context

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

WithDeadline

package context

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		// The current deadline is already sooner than the new one.
		return WithCancel(parent)
	}
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
	propagateCancel(parent, c)
	dur := time.Until(d)
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded) // deadline has already passed
		return c, func() { c.cancel(false, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}

timerCtx

package context

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
}

func (c *timerCtx) cancel(removeFromParent bool, err error) {
	c.cancelCtx.cancel(false, err)
	if removeFromParent {
		// Remove this timerCtx from its parent cancelCtx's children.
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}

A bidirected graph node

Demo

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

const waitFor = ... to be defined

func main() {
	ctx := context.Background()
	ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
	wg := &sync.WaitGroup{}

	for i := 0; i < 3; i++ {
		wg.Add(1)
		go schedule(ctx, wg, i)
	}

	time.Sleep(waitFor)
	cancel()
	wg.Wait()
	fmt.Println("done")
}

func schedule(ctx context.Context, wg *sync.WaitGroup, i int) {
	defer wg.Done()
	switch i {
	case 0:
		ctx, cancel := context.WithTimeout(ctx, time.Second)
		defer cancel()
		work(ctx, fmt.Sprintf("worker %d had a timeout of 1 second", i))
	case 1:
		ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
		defer cancel()
		work(ctx, fmt.Sprintf("worker %d had a timeout of 10 second", i))
	case 2:
		ctx, cancel := context.WithTimeout(ctx, -1*time.Second)
		defer cancel()
		work(ctx, fmt.Sprintf("worker %d had a timeout of -1 second", i))
	}
}

func work(ctx context.Context, prefix string) {
	ctx = context.WithValue(ctx, "name", "Damiano")
	ctx = context.WithValue(ctx, "surname", "Petrungaro")

	fmt.Printf("%s: started\n", prefix)
	select {
	case <-time.Tick(3 * time.Second):
		fmt.Printf("%s: finished\n", prefix)
	case <-ctx.Done():
		fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
	}
}

Demo

(name = Damiano)

(surname = Petrungaro)

(name = Damiano)

(surname = Petrungaro)

(name = Damiano)

(surname = Petrungaro)

main

schedule

work

1s

10s

-1s

7s

Demo

(name = Damiano)

(surname = Petrungaro)

(name = Damiano)

(surname = Petrungaro)

(name = Damiano)

(surname = Petrungaro)

main

schedule

work

1s

10s

-1s

7s

Fun fact

How can the context package be tested if the testing package requires the context one?

Fun fact

package context_test

import (
	. "context"
	"testing"
)

func TestBackground(t *testing.T)                      { XTestBackground(t) }
func TestTODO(t *testing.T)                            { XTestTODO(t) }
func TestWithCancel(t *testing.T)                      { XTestWithCancel(t) }
func TestParentFinishesChild(t *testing.T)             { XTestParentFinishesChild(t) }
func TestChildFinishesFirst(t *testing.T)              { XTestChildFinishesFirst(t) }

// ...

src/context/x_test.go

Fun fact

package context

//...

type testingT interface {
	Deadline() (time.Time, bool)
	Error(args ...interface{})
	Errorf(format string, args ...interface{})
	Fail()
	FailNow()
	Failed() bool
	Fatal(args ...interface{})
	Fatalf(format string, args ...interface{})
	Helper()
	Log(args ...interface{})
	Logf(format string, args ...interface{})
	Name() string
	Parallel()
	Skip(args ...interface{})
	SkipNow()
	Skipf(format string, args ...interface{})
	Skipped() bool
}

//...

func XTestBackground(t testingT) {...}

func XTestTODO(t testingT) {...}

func XTestWithCancel(t testingT) {...}

//...

src/context/context_test.go

Recap

(key = value)

timer

emptyCtx

valueCtx

cancelCtx

timerCtx

twitter: @damiano_dev
email: damianopetrungaro@gmail.com