background

Context is a new feature introduced in Go 1.7. It is mainly used for information transfer between Groutine, which is a unique attribute of Go. The following content is mainly about some interpretation of the source code and some thoughts on the use of Context, and exchange with community partners ~ 😁

The source code interpretation

Context is one of the most important interfaces in the Context package.

type Context interface {
   Deadline() (deadline time.Time, ok bool)
   Done() <-chan struct{}
   Err() error
   Value(key interface{}) interface{}}Copy the code
  • DeadLine() => Returns the time when the Context was cancelled, which corresponds to the expiration date of the Context
  • Done() => return a channel. Note that this channel is read-only.Context is not sent to this channel, and the only way to read from this channel is to close it. This feature is the key to communication between Groutine
  • Err() => returns an error, which is a fixed set of errors in context. context, as discussed later
  • Value() => Context can be used to store data, and this function can obtain the corresponding Value from the stored key

Built-in error types

Canceled Canceled is used after a Context has been Canceled, and DeadlineExceeded is defined as a Canceled Context.

// Canceled is the error returned by Context.Err when the context is canceled.
var Canceled = errors.New("context canceled")

// DeadlineExceeded is the error returned by Context.Err when the context's
// deadline passes.
var DeadlineExceeded error = deadlineExceededError{}

type deadlineExceededError struct{}

func (deadlineExceededError) Error(a) string   { return "context deadline exceeded" }
func (deadlineExceededError) Timeout(a) bool   { return true }
func (deadlineExceededError) Temporary(a) bool { return true }
Copy the code

Built-in Context type

Context.Context defines four types, from simple to complex, and describes them one by one

  • emptyCtx

EmptyCtx is an empty type defined by Context. The methods used in common projects are context.background () and context.todo (). These methods correspond to Background and TODO defined in Context, respectively. As you can see, background and TODO are shared in the same Go project

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
}

func (e *emptyCtx) String(a) string {
   switch e {
   case background:
      return "context.Background"
   case todo:
      return "context.TODO"
   }
   return "unknown empty Context"
}
Copy the code
  • valueCtx

ValueCtx is a Context that is used to store and read values.

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

It empowers the Context(interface), so that even if it doesn’t implement all the methods of the Context interface, it still belongs to a Context(interface), which is a feature of Go

The methods associated with valueCtx initialization are defined 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() {panic("key is not comparable")}return &valueCtx{parent, key, val}
}
Copy the code

This method returns a valueCtx embedded in the parent Context. Note that the key passed must be comparable, otherwise you cannot determine whether a key=>value pair exists by key equality

func (c *valueCtx) String(a) string {
   return contextName(c.Context) + ".WithValue(type " +
      reflectlite.TypeOf(c.key).String() +
      ", val " + stringify(c.val) + ")"
}

func (c *valueCtx) Value(key interface{}) interface{} {
   if c.key == key {
      return c.val
   }
   return value(c.Context, key)
}
Copy the code

The String() method is mainly used for printing. The most important thing for valueCtx is the design of the Value() method. Each instance of valueCtx stores at most one pair of keys => Value. So when the caller calls the Value method to obtain the Value of a key, the result is:

EmptyCtx (Background/TODO); / / emptyCtx(Background/TODO);

func value(c Context, key interface{}) interface{} {
   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 *timerCtx:
         if key == &cancelCtxKey {
            return &ctx.cancelCtx
         }
         c = ctx.Context
      case *emptyCtx:
         return nil
      default:
         return c.Value(key)
      }
   }
}
Copy the code

As you can see, in the value method, you declare a for loop that terminates if you find the key in some parent Context or go to emptyCtx


Before we get to the next two types of Context, let’s look at one interface. Since the other two contexts implement this interface, we need to mention that its definition looks like this:

type canceler interface {
   cancel(removeFromParent bool, err error)
   Done() <-chan struct{}}Copy the code

The Context that implements the interface definition method is a Context that can be cancelled

  • cancelCtx
type cancelCtx struct {
   Context

   mu       sync.Mutex            
   done     atomic.Value          
   children map[canceler]struct{} 
   err      error                 
}
Copy the code

Mu is mainly used to protect children and done channels in concurrent scenarios. Let’s take a look at all the methods related to cancelCtx

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

An additional step to cancelCtx initialization is to call propagateCancel. This method attaches the currently created cancelCtx to a parent node, if the parent node is cancelable, This synchronizes the signal to the current cancelCtx when the parent cancels, causing it to cancel as well

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()
      ifp.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(a) {
         select {
         case <-parent.Done():
            child.cancel(false, parent.Err())
         case <-child.Done():
         }
      }()
   }
}
Copy the code

In propagateCancel, first check whether the parent Context of the current cancelCtx can be cancelled, if not, the current cancelCtx does not need to be mounted on the parent Context, and then if the parent Context can be cancelled and has been cancelled, CancelCtx is cancelled directly. Otherwise, call parentCancelCtx to check whether the current parent Context is standard cancelCtx, and perform corresponding logical processing

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
   done := parent.Done()
   if done == closedchan || done == nil {
      return nil.false
   }
   p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
   if! ok {return nil.false
   }
   pdone, _ := p.done.Load().(chan struct{})
   ifpdone ! = done {return nil.false
   }
   return p, true
}
Copy the code

Let’s focus on this method. At the beginning of this method, WE don’t know why there are so many judgment conditions, and we don’t know whether the friends have the same feeling.

  1. The current parent Context is of type cancelCtx
  2. The current parent Context is of type timerCtx
  3. The current parent Context is of a custom Context type

The first condition

 done := parent.Done()
 if done == closedchan || done == nil {
    return nil.false
 }
Copy the code

This condition checks to see if the current parent Context is closed, and if so, returns directly

Second condition

p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
if! ok {return nil.false
}
Copy the code

If the Context type is 1,2, the value will return true(cancelCtx embedded in timerCtx is of the cancelCtx type). If the Context type is user-defined, the value will return true. What if I can satisfy this judgment condition? I’m going to give you two demos, I believe you can understand 😊, both of which can pass condition 2

demo one:

type MyContext1 struct {
   Context context.Context
}

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

func (my *MyContext1) Done(a) <-chan struct{} {
   return make(chan struct{})}func (my *MyContext1) Err(a) error {
   return nil
}

func (my *MyContext1) Value(key interface{}) interface{} {
   return my.Context
}

func (*MyContext1) cancel(removeFromParent bool, err error) {
   return
}

func main(a) {
   parentCtx, _ := context.WithCancel(context.Background())
   cur := &MyContext1{parentCtx}
   childCtx, _ := context.WithCancel(cur)
   fmt.Println(childCtx)
}
Copy the code

demo two:

type MyContext2 struct {
   context.Context
}

func main(a) {
   parentCtx, _ := context.WithCancel(context.Background())
   cur := &MyContext2{parentCtx}
   childCtx, _ := context.WithCancel(cur)
}
Copy the code

The third condition

pdone, _ := p.done.Load().(chan struct{})
ifpdone ! = done {return nil.false
}
Copy the code

For a custom Context, as demo One implemented, this case will not pass, but it will work for Demo Two because the MyContext implementation does not have a custom Done method, that is, it is manageable for context.context

If the current parent Context is not cancelCtx, that is, not controllable for context.Context, then a coroutine will start and listen for the Done method of the parent Context and the Done method of the current cancelCtx. Why listen on the current cancelCtx Done method? In case the parent Context is never Done and the goroutine leaks

Next, let’s look at the most important method in cancelCtx: the cancel method

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 // already canceled
   }
   c.err = err
   d, _ := c.done.Load().(chan struct{})
   if d == nil {
      c.done.Store(closedchan)
   } else {
      close(d)
   }
   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)
   }
}
Copy the code

The process of the cancel method is to close the done of the current Context (read only channel), iterate over all of the current Context’s children, cancel them one by one, and remove itself from the parent Context

A careful partner might notice that the argument passed in the call to child.cancel(false, err) is false, while the argument passed in withCancel is true, This is because calling child.cacel(false, err) means that the parent Context has been canceled, and then the parent Context will set children to nil, so it’s not necessary to pass true, And setting it to true in withCancel means that if the child Context calls the cancel method itself and the parent doesn’t cancel, then the child Context needs to remove itself from the parent Context

  • timerCtx

So let’s look at the definition

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

   deadline time.Time
}
Copy the code

TimerCtx has built-in cancelCtx, that is, it can be used as cancelCtx, and it also adds a timer and an expiration deadline

TimerCtx sets a timed out Context. For example, if the downstream Context does not return a result after 1s, the Context will cancel and the timerCtx method will be declared as:

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

Since the body of this method is actually WithDeadline, let’s look directly at this method:

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

CancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx After that, bind the timerCtx to the parent Context and determine whether the current time is earlier than the current time. If so, cancel the timerCtx directly. Otherwise, run the time.AfterFunc command to set a scheduled task and execute the timerCtx cancel method when the time reaches

TimerCtx also has its own cancel method, which simply stops the timerCtx timer compared to cancelCtx’s cancel

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

Use the pose

We can use valueCtx to synchronize data between goroutines. We can use cancelCtx to prevent Goroutine leaks. We can use timerCtx to set the response timeout of the interface. Let me show you how I use these features

Synchronize data between goroutines

func Process(ctx context.Context) {
   name := ctx.Value("name"). (string)
   fmt.Println(name)
}

func main(a) {
   rootCtx := context.Background()
   childCtx := context.WithValue(rootCtx, "name"."Jack")
   Process(childCtx)
}
Copy the code

I will not say more about this way of passing value, I believe that partners have been flexible grasp

Prevent goroutine leaks

func Process(ctx context.Context) {
   for {
      select {
      case <-ctx.Done():
         fmt.Println("cancel happen")
         return
      default:
         time.Sleep(1 * time.Second)
         fmt.Println("hello jhd")}}}func main(a) {
   rootCtx := context.Background()
   parentCtx, parentCancel := context.WithCancel(rootCtx)
   childCtx, _ := context.WithCancel(parentCtx)

   go Process(childCtx)
   time.Sleep(2 * time.Second)

   parentCancel()
   time.Sleep(3 * time.Second)
}
Copy the code

A child goroutine is derived from the parent groutine to ensure that the parent goroutine is cancelled and the child goroutine is cancelled without leakage of goroutine

Example Set the response timeout of the interface

func AnoProcess(a) chan struct{} {
   c := make(chan struct{}, 1)
   defer func(a) {
      c <- struct{}{}
   }()
   time.Sleep(time.Second * 6)
   return c
}

func Process(ctx context.Context) {
   res := make(chan struct{}, 1)
   go func(a) {
      res = AnoProcess()
   }()

   select {
   case <-ctx.Done():
      fmt.Println(ctx.Err())
      return
   case <-res:
      fmt.Println("hello jhd")
      return}}func main(a) {
   rootCtx := context.Background()
   ctx, _ := context.WithTimeout(rootCtx, time.Second*3)

   fmt.Println("start exec func at ", time.Now().Second())
   Process(ctx)
   fmt.Println("end exec func at ", time.Now().Second())
}
Copy the code

Here we set the AnoProcess method a timeout of 3s, after which if the AnoProcess has not finished, it returns directly

conclusion

Ok, this is all I share this time, the community partners have any comments, questions can be put in the comment section, I hope to get everyone’s feedback, your feedback is the biggest motivation for me to continue to create!!