Goroutine mechanism in go language is naturally suitable for server development. Recently, I saw the operation about context when I looked at some framework code inside goose factory. Although channel can handle communication between different Goroutines very well, But context is a great place to do cancelation-related actions, and it can be useful in many situations. The context code in the GO source code is not very long, so I will briefly review it today.

Context

Context source code in this article comes from GO1.15

Context is a structure used to convey context information, which is actually an interface, as follows:

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

The Deadline() method returns two arguments, one Deadline, of type time. time, which means the time line when the context was cancelled, and the second bool, which returns false if a context has no Deadline set.

What is context canceled? A context that is set to expire is cancelled; If cancel() is performed in the code, the context is also cancelled (see more below)

The Done() method returns a struct{} channel for passing messages between different goroutines. This is usually used in conjunction with SELECT. When this channel is closed, it returns the corresponding value of 0.

The Err() method returns an error in one of the following cases:

  • whenDoneIn thechannelReturn when it has not been closednil
  • whenDoneIn thechannelWhen closed, returnCorresponding causes, such as being normalCanceledIs it out of dateDeadlineExceeded.

Value() returns the corresponding Value in the context based on the input key, which can be used to pass arguments.

Default context

The default context Background and TODO are provided in Go, both of which return an empty context — emptyCtx. Context.Backgroud() is used when no context exists in code but is needed.

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

EmptyCtx implements all the interfaces of the context, but they are empty.

type emptyCtx int

func (*emptyCtx) Deadline(a) (deadline time.Time, ok bool) {
	return
}

func (*emptyCtx) Done(a) <-chan struct{} {
	return nil
}

func (*emptyCtx) Err(a) error {
	return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
	return nil
}
Copy the code

cancelCtx

The emptyCtx mentioned above does nothing, whereas cancelCtx cancels the context and then changes the state of the context by Done.

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
}
Copy the code

CancelCtx defines the Context field anonymously, cancelCtx creates the children field anonymously, cancelCtx creates the children field anonymously, cancelCtx creates the children field anonymously, cancelCtx creates the children field anonymously, cancelCtx creates the children field anonymously, cancelCtx creates the children field anonymously, cancelCtx creates the children field anonymously, cancelCtx creates the children field anonymously, cancelCtx creates the children field anonymously.

type canceler interface {
	cancel(removeFromParent bool, err error)  // removeFromParent if true
                                                  // The context is removed from its parent context
	Done() <-chan struct{}}Copy the code

Create a context with WithCancel, source code:

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(a) { c.cancel(true, Canceled) }
}
Copy the code

The created context can be cancelled either if the returned cancel() function is executed or if the parent is cancelled at the time of creation.

func main(a) {
	ctxParent, cancelParent := context.WithCancel(context.Background())
	ctxChild, _ := context.WithCancel(ctxParent)
	// Parent CTX execution cancelled
	cancelParent()

	select {
	case <-ctxParent.Done():
		fmt.Println("Parent CTX cancelled")}select {
	case <-ctxChild.Done():
		fmt.Println("Child CTX cancelled")}}Copy the code

The above code outputs two lines

Parent CTX cancelled child CTX cancelledCopy the code

The reason is that the child CTX cancels after the parent CTX cancels. Call cancelCtx (newCancelCtx) and propagateCancel (*) within WithCancel, call cancelCtx (*), Cancel is how to close a channel in the context, which is pretty simple.

timerCtx

TimerCtx contains a timer and a deadline. When the timer ends, the cancelCtx method will be called to cancel the context.

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

	deadline time.Time
}
Copy the code

To create a context with a timer, use WithDeadline or WithTimeout.

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(a) { c.cancel(false, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func(a) {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func(a) { c.cancel(true, Canceled) }
}
Copy the code

C. Temer = time.afterfunc (dur, func() {c.ancel (true, DeadlineExceeded)}) Execute the c. canel method after duR time. The source code for this method is as follows:

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

Cancel (false, err) cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx

valueCtx

The context package uses valueCtx to pass key-value pairs. The structure is as follows (it couldn’t be simpler…) :

type valueCtx struct {
	Context
	key, val interface{}}Copy the code

ValueCtx also contains Context as an anonymous interface, so it also has the properties of Context. Use WithValue to set a context with key-value. Use value to recursively find the value corresponding to the key. The source code is as follows:

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() {// It must be comparable, otherwise the Value method will not work
		panic("key is not comparable")}return &valueCtx{parent, key, val}
}
Copy the code
func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)   // Note that this is a go language feature, similar to the inheritance feature in Java
                                      // valueCtx inherits the properties of Context
}
Copy the code

conclusion

Finally, context contexts are great for passing simple messages, key-value values, but using context too often can cause your code to have a context everywhere, because you always need to pass context as an argument to your functions…