Skip to content

context

context is named as context, and its original design purpose is to transmit signals and some simple data across multiple goroutines, especially between parent and child goroutines. It's commonly used in scenarios such as handling HTTP requests, task scheduling, database queries, etc. Especially in microservice architectures, gRPC uses context for cross-process and cross-network metadata transmission, link control operations, etc.

go
package main

import (
    "context"
    "fmt"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM)
    defer stop()
    for {
       select {
       case <-ctx.Done():
          fmt.Println("terminate")
          return
       default:
       }
       fmt.Println("running")
       time.Sleep(100 * time.Millisecond)
    }
}

In the above example, process signals are transmitted through context. When a signal is received, the program exits itself. This is also an application scenario of context.

Structure

context.Context is not a concrete implementation, but an interface defining a set of methods:

go
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
	Deadline() (deadline time.Time, ok bool)

	Done() <-chan struct{}

	Err() error

	Value(key any) any
}
  • Deadline(), returns a deadline and a boolean indicating whether it has ended
  • Done(), returns a channel used to notify end messages
  • Err(), returns the reason for context closure
  • Value(), gets the specified value according to key

There are the following usable contexts in the standard library:

  • Background, blank context, usually used as the root node of contexts
  • WithCancel, WithCancelCause, cancellable context
  • WithDeadline, WithDeadlineCause, context with deadline
  • WithTimeout, WithTimeoutCause, context with timeout
  • WithValue, context that can carry values

Specifically, the main implementations are:

  • timerCtx
  • cancelCtx
  • emptyCtx

So its core functionality has four points:

  • Cancel
  • Deadline
  • Value passing
  • Propagation

Understanding these points basically clarifies how context works.

Cancel

go
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
	cause    error                 // set to non-nil by the first cancel call
}


func withCancel(parent Context) *cancelCtx {
    if parent == nil {
       panic("cannot create context from nil parent")
    }
    c := &cancelCtx{}
    c.propagateCancel(parent, c)
    return c
}

The core of cancelCtx lies in the propagateCancel method, which is responsible for propagating the cancellable behavior to parent and child contexts.

go
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
	...
}
  1. First, it checks whether the parent context can be canceled. If not, it returns directly.

    go
    done := parent.Done()
    if done == nil {
    	return // parent is never canceled
    }
  2. Returns and checks whether the parent context has already been canceled. If yes, it cancels all child contexts.

    go
    select {
    case <-done:
    	// parent is already canceled
    	child.cancel(false, parent.Err(), Cause(parent))
    	return
    default:
    }
  3. Tries to convert the parent context to cancelCtx type. If successful, it adds the current context to the parent context's children.

    go
    if p, ok := parentCancelCtx(parent); ok {
    	// parent is a *cancelCtx, or derives from one.
    	p.mu.Lock()
    	if p.err != nil {
    		// parent has already been canceled
    		child.cancel(false, p.err, p.cause)
    	} else {
    		if p.children == nil {
    			p.children = make(map[canceler]struct{})
    		}
    		p.children[child] = struct{}{}
    	}
    	p.mu.Unlock()
    	return
    }
  4. Tries to convert it to afterFuncer type. If successful, it registers the method for canceling the current context to the parent context's AfterFunc.

    go
    if a, ok := parent.(afterFuncer); ok {
    	// parent implements an AfterFunc method.
    	c.mu.Lock()
    	stop := a.AfterFunc(func() {
    		child.cancel(false, parent.Err(), Cause(parent))
    	})
    	c.Context = stopCtx{
    		Context: parent,
    		stop:    stop,
    	}
    	c.mu.Unlock()
    	return
    }
  5. If still not working, it starts a separate goroutine to listen to the Done channel. When a signal is received, it cancels the child context.

    go
    go func() {
        select {
        case <-parent.Done():
           child.cancel(false, parent.Err(), Cause(parent))
        case <-child.Done():
        }
    }()

Then the cancelCtx.cancel method is ultimately responsible for canceling child contexts:

go
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
	...
}

Its flow is as follows:

  1. Check whether the current context has already been canceled.

    go
    if err == nil {
    	panic("context: internal error: missing cancel error")
    }
    if cause == nil {
    	cause = err
    }
    c.mu.Lock()
    if c.err != nil {
    	c.mu.Unlock()
    	return // already canceled
    }
  2. Close the done channel and send closure notification.

    go
    c.err = err
    c.cause = cause
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
    	c.done.Store(closedchan)
    } else {
    	close(d)
    }
  3. Traverse and notify child contexts.

    go
    for child := range c.children {
    	child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()
  4. Finally, determine whether to remove from the parent context based on the passed parameter.

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

Deadline

WithTimeout and WithDeadline are both contexts with deadlines. They are the same type, just with different semantics, and both are based on cancelCtx.

go
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

WithDeadlineCause is responsible for creating contexts with deadlines:

go
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
	...
}

Its flow is as follows:

  1. Check the deadline. If the parent deadline is earlier than the current deadline, then the parent context will definitely be canceled before the current context, so directly create a cancelCtx type context.

    go
    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)
    }
  2. Build timerCtx and propagate to child contexts.

    go
    c := &timerCtx{
    	deadline: d,
    }
    c.cancelCtx.propagateCancel(parent, c)
  3. Calculate the current deadline. If already expired, cancel directly.

    go
    dur := time.Until(d)
    if dur <= 0 {
    	c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
    	return c, func() { c.cancel(false, Canceled, nil) }
    }
  4. If not expired, use time.AfterFunc to set canceling the current context at the deadline.

    go
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
    	c.timer = time.AfterFunc(dur, func() {
    		c.cancel(true, DeadlineExceeded, cause)
    	})
    }
    return c, func() { c.cancel(true, Canceled, nil) }

For timerCtx, its method for canceling the context just stops the timer and also stops child contexts.

go
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
	c.cancelCtx.cancel(false, err, cause)
	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()
}

Value Passing

valueCtx can pass and get values in contexts:

go
type valueCtx struct {
    Context
    key, val any
}

func WithValue(parent Context, key, val any) 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}
}

Its core lies in the value function. Other context concrete implementations also enter this function to get values, such as:

go
func (c *valueCtx) Value(key any) any {
	if c.key == key {
		return c.val
	}
	return value(c.Context, key)
}

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

The value function is a large for loop that continuously recurses upward to find the specified key's value:

go
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 *cancelCtx:
			if key == &cancelCtxKey {
				return c
			}
			c = ctx.Context
		case withoutCancelCtx:
			if key == &cancelCtxKey {
				// This implements Cause(ctx) == nil
				// when ctx is created using WithoutCancel.
				return nil
			}
			c = ctx.c
		case *timerCtx:
			if key == &cancelCtxKey {
				return &ctx.cancelCtx
			}
			c = ctx.Context
		case backgroundCtx, todoCtx:
			return nil
		default:
			return c.Value(key)
		}
	}
}

Handling of different Context types:

  • *valueCtx: If key matches the current Context's key, return the val corresponding to key.
  • *cancelCtx: If key matches cancelCtxKey, return the current cancelCtx, meaning return itself.
  • withoutCancelCtx: Represents a Context without cancel functionality. If key matches cancelCtxKey, return nil.
  • *timerCtx: If key matches cancelCtxKey, return its associated cancelCtx.
  • backgroundCtx and todoCtx: Usually special types of Contexts that don't carry any extra values. When encountering these two types, return nil directly.
  • If unknown type, continue calling Value method to search.

Summary

The core of these contexts is cancelCtx, which propagates cancel signals. Other context types and even third-party context types are layered on top of this, implementing various functionalities based on this foundation.

Golang by www.golangdev.cn edit