When developing with Go as a server, each incoming request is assigned a Goroutine to handle, and additional Goroutines may be created to access DB or RPC services during request processing. The goroutine involved in this request may require access to specific values such as authentication token, user id, or request expiration time. When a request is cancelled or times out, the goroutine involved in the request should also be terminated so that the system can quickly reclaim the resource.

For simplicity purposes, the context package defines the context type to pass timeout, cancel signals, and Request-scope values across API boundaries and between processes. When the server makes a new request it should create a Context and the return request should accept a Context. The chain of function calls must pass the Context object, or a Context derived from WithCancel, WithDeadline, WithTime, or WithValue. When a Context is cancelled, all contexts derived from that object are cancelled.

For example, you can implement a time-out protection method using the context mechanism

func main(a) {
	cancelJob(time.Second*1.func(a) error {
		time.Sleep(time.Second * 10)
		return nil})}func cancelJob(timeout time.Duration, f func(a) error) error {
	var (
		ctx        context.Context
		cancelFunc context.CancelFunc
	)
	if timeout > 0 {
		ctx, cancelFunc = context.WithTimeout(context.Background(), timeout)
	} else {
		ctx, cancelFunc = context.WithCancel(context.Background())
	}

	defer cancelFunc()
	e := make(chan error, 1)
	go func(a) {
		e <- f()
	}()
	select {
	case err := <-e:
		return err
	case <-ctx.Done():
		return ctx.Err()
	}
}
Copy the code

Context uses the general steps:

  • 1. Build aContextObject, if you don’t know what Context to use, you can call it, okaycontext.Backgroundorcontext.TODO
  • 2 According to your needsContextPackage derivative
    • WithCancel :Context can be cancelled
    • WithDeadline: Context You can set the deadline
    • WithTimeout: Actually uses WithDeadline
    • WithValue: Uses Context to pass the value
  • 3 Listen for the channel ctx.Done and the channel will be closed once the Context is cancelled
  • 4 At last, call the cancel method to facilitate resource reclamation

The data structure

The Context package provides two convenient ways to create context objects

  • Context.Background cannot be cancelled. It has no value and no expiration date, and is usually used for main functions, initializations, tests, or as the top-level context when a new request comes in
  • Context.todo is used when you don’t know what context to use

Both methods are an emptyCtx object essentially the same

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)
func Background(a) Context {
	return background
}
func TODO(a) Context {
	return todo
}
Copy the code

In addition, the context package provides four methods: WithCancel, WithDeadline, WithTimeout, and WithValue can be derived from context to cancelCtx, timerCtx, and valueCtx, all of which implement the context. context interface

Context's methods are thread-safe
type Context interface {
  // Returns when context needs to be cancelled. Ok false means deadline is not set
	Deadline() (deadline time.Time, ok bool)
	// Put back a closed channel when context is cancelled Done
  // Done returns nil to indicate that the current context cannot be cancelled
	// Done is usually used in select statements
	Done() <-chan struct{}
  // Returns the reason for the context cancellation
	Err() error
  // Returns the value associated with the key specified in context, nil if not specified
	// Mainly used for request-scoped data passing between process and API boundary, not for optional parameter passing
	// Key needs to support equality operations and is best defined as not being able to type everywhere to avoid confusion
	Value(key interface{}) interface{}}Copy the code

These object hierarchies

Derivative contexts

Context derived with WithCancel, WithDeadline, WithTimeout, and WithValue provides functions such as cancellation, value transfer, and timeout cancellation for the original context.

WithCancel

WithCancel spawns a new cancelable Context object

// WithCancel returns the context containing a copy of the parent context and a new channel and a cancel function
// When the returned cancel function is called or the parent context's Done channcel is closed, the channel of the context returned by WitchCancel is also closed
// The cancel function should be called as soon as the operation is complete so that the resources associated with this context can be released
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
  // Build the relationship between parent and child contexts to ensure that the child context is cancelled when the parent context is cancelled
	propagateCancel(parent, &c)
	return &c, func(a) { c.cancel(true, Canceled) }
}
Copy the code

Wrap Context as cancelable Context–>cancelCtx

CancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx
type cancelCtx struct {
	Context

	mu       sync.Mutex            // Protect the following fields
	done     chan struct{}         // Lazy creates the cancel method to close the first time it is called
	children map[canceler]struct{} // Cancel is set to nil on the first call
	err      error                 // Cancel is set non-nil the first time it is called
}
Copy the code

The done channel is initialized only when cancelCtx is called. If cancelCtx subcontext can be cancelled, the Canceler interface needs to be implemented

// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
	cancel(removeFromParent bool, err error)
	Done() <-chan struct{}}Copy the code

If the parent context’s Done is nil, it cannot be cancelled. Otherwise, parentCancelCtx will be called until the cancelable parent context is found. If it is found, parentCancelCtx will be called

  • If you can find

    • If the parent context has been cancelled, the child context’s cancel method is called to cancel;
    • If the parent context is not cancelled, the current child context is managed by the parent context
  • If a developer custom type cannot be found, for example

    Launch a Gorountine directly to listen for notifications of parent-child cancellations

// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
	if parent.Done() == nil {
		return // parent is never canceled
	}
	if p, ok := parentCancelCtx(parent); ok {// Find the parent and cancel the context
		p.mu.Lock()
		ifp.err ! =nil {
			// The parent context has been cancelled
			child.cancel(false, p.err)
		} else {// If the parent context does not cancel, the child context is given to the parent context to manage, so that the parent node can propagate the cancellation event to the child context when it cancels
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {// Cancel context if the parent is not found
		go func(a) {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}
Copy the code

ParenCancelCtx loops to see if the context has a cancelable parent

// parentCancelCtx follows a chain of parent references until it finds a
// *cancelCtx. This function understands how each of the concrete types in this
// package represents its parent.
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
	for {
		switch c := parent.(type) {
		case *cancelCtx:
			return c, true
		case *timerCtx:
			return &c.cancelCtx, true
		case *valueCtx:
			parent = c.Context
		default:
			return nil.false}}}Copy the code
Cancel the function cancel

When your business method is finished executing, you should call the Cancel method as soon as possible to reclaim resources quickly

If removeFromParent is true, c is removed from the parent's child context
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	c.mu.Lock()
	ifc.err ! =nil {
		c.mu.Unlock()
		return // if c.elr is not nil, the current context has been cancelled
	}
	c.err = err
	if c.done == nil { // Calling the cancel method is initialized at this point
		c.done = closedchan//closedchan is the closedchan as its name indicates
	} else {/ / close the c.d one
		close(c.done)
	}
  // Cancel the subcontext
	for child := range c.children {
		// The parent context lock is held and the child context lock is acquired
		child.cancel(false, err)
	}
	c.children = nil // Cancel completes set nil
	c.mu.Unlock()

	if removeFromParent {// Remove the current context from its parent's child context collection
		removeChild(c.Context, c)
	}
}
Copy the code

WithDeadline

Make context timeout cancel

// WithDeadline returns a context containing a copy of the parent context and a context WithDeadline d, if the parent deadline is earlier than d
WithDeadline(parent, d) is semantically equivalent to the parent context
The done channel in the context returned by WithDeadline is also closed when the deadline expires or the returned cancel function is called or the parent context's Done channel is closed
// The cancel function should be called as soon as the operation is complete so that the resources associated with this context can be released
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	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) // The deadline has expired
		return c, func(a) { c.cancel(false, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
    // Time monitor
		c.timer = time.AfterFunc(dur, func(a) {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func(a) { c.cancel(true, Canceled) }
}
Copy the code

TimerCtx is embedded with cancelCtx, and the main cancel capability is brokered. In addition, an expiration time and a timer are added. If the expiration time is not reached and the context is not cancelled, a timer will be started. If the context expires, cancel will be performed

// timerCtx contains a timer and a cancelCtx embedded within it to implement the Done and Err methods
// Cancel by stopping the timer and then calling cancelctx.cancel
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}
Copy the code

The Cancel operation of timerCtx itself stops the timer, and then the main Cancel operation agent is given to cancelCtx

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()
	ifc.timer ! =nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}
Copy the code

WithTimeut

The actual use of WithDeadline is nothing to say.

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

So basically, the cancelable context holds the collection of children through the parent and if the parent cancels, then the context in the collection of children is called cancel in turn.

WithValue

Add a Value(key interface{}) interface{} method to get the Value associated with the Context based on the specified key. The logic is simple and there’s nothing to talk about.

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

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
	Context
	key, val interface{}}Copy the code

conclusion

The core implementation of the Context package is only 200-300 lines, excluding comments. The overall implementation is still short and concise, providing data transfer across process and API boundaries, as well as concurrency and timeout cancellation. The practical application process also brings great convenience to our technical implementation, such as the implementation of full link trace. It is recommended that we use the context as the first parameter of the function, but it is still a mental burden for many people to use it. Therefore, some people make a Goroutine local storage to avoid writing the context