Golang's Context introduction and source code analysis

Introduction

In the Go service, for each request, a coroutine will be processed. In the process of coroutines, many coroutines are also used to access resources, such as databases, such as RPC. These coroutines also need to access some information of the request dimension, such as the identity of the requester, authorization information, and so on. When a request is cancelled or times out, all other coroutines should be cancelled immediately to release resources.
Golang's context package is used to transfer the data, signals, and timeout of the requested dimension to all coroutines that process the request. In the method call chain that handles the request, the context must be passed. Of course, you can also use WithCancel, WithDeadline, WithTimeout, or WithValue to derive a child context to pass. When a context is canceled, all the child contexts derived from it will be canceled.
Introduction of the package

Context

In the context package, the core is the Context interface, which has the following structure:

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

A context can pass a deadline, a cancellation signal, and other values. The method can be called by multiple coroutines at the same time.

  • Deadline () (deadline time.Time, ok bool)
    returns a deadline, which means that the context will be canceled if it reaches the deadline. If the returned ok is false, no cut-off time has been set, that is, it will not time out.

  • Done () <-chan struct {}
    returns a closed channel, indicating that the context has been cancelled. If the context can never be cancelled, then return nil.
    Generally Done is used in the select statement, such as:

func Stream(ctx context.Context, out chan<- Value) error {//Stream函数就是用来不断产生值并把值发送到out channel里,直到发生DoSomething发生错误或者ctx.Done关闭
   for {
      v, err := DoSomething(ctx)//DoSomething用来产生值
      if err != nil {
         return err
      }
      select {
      case <-ctx.Done(): //ctx.Done关闭
         return ctx.Err()
      case out <- v://将产生的值发送出去
      }
   }
}

  • The err returned by Err () error is used to explain the reason for the cancellation of this context. If the Done channel has not been closed, Err () returns nil.
    The two errors that come with the general package are: cancellation and timeout
var Canceled = errors.New("context canceled")
var DeadlineExceeded error = deadlineExceededError{}
  • Value (key interface {}) interface {}
    returns the value corresponding to the key stored in the context, or nil if it does not exist.

WithCancel

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
Copy the parent context and assign a new Done channel. Return this context and cancel function.
When one of the following occurs, the context will be canceled:

  • cancel function is called
  • The Done channel of the parent context is closed,
    so when writing code, if logic processing has been completed in the current context, the cancel function should be called to notify other coroutines to release resources.
    Examples of use:
func main() {
   //gen函数用来产生一个不断生产数字到channel的协程,并返回channel
   gen := func(ctx context.Context) <-chan int {
      dst := make(chan int)
      n := 1
      go func() {
         for {
            select {
            case <-ctx.Done():
               return //每一次生产数字都检查context是否已经被取消,防止协程泄露
            case dst <- n:
               n++
            }
         }
      }()
      return dst
   }

   ctx, cancel := context.WithCancel(context.Background())
   defer cancel() //在消费完生产的数字之后,调用cancel函数来取消context,以此通知其他协程

   for n := range gen(ctx) { //只消费5个数字
      fmt.Println(n)
      if n == 5 {
         break
      }
   }
}

Source code analysis

How is this method implemented? Let's take a look at the source code:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
   c := newCancelCtx(parent)
   propagateCancel(parent, &c)
   return &c, func() { c.cancel(true, Canceled) }
}

It can be seen that there are mainly two methods: newCancelCtx and propagateCancel
newCancelCtx is to initialize a cancelCtx object with the parent context, and cancelCtx is an implementation class of context:

func newCancelCtx(parent Context) cancelCtx {
   return cancelCtx{Context: parent}
}

Looking at the structure of this newCancelCtx, newCancelCtx is actually an implementation class of the Context interface:

type cancelCtx struct {
   Context //指向父context的引用

   mu       sync.Mutex            // 锁用来保护下面几个字段
   done     chan struct{}         // 用来通知其他,代表这个context已经结束了
   children map[canceler]struct{} // 里面保存了所有子contex的联系,用来在结束当前context的时候,结束所有子context。这个字段cancel之后,设为nil。
   err      error                 // cancel之后,就不是nil了。
}

As the name implies, propagateCancel is to propagate cancel, which is to ensure that when the parent context ends, the child context we get with WithCancel can follow the end.
Then return to the newly created cancelCtx object and a CancelFunc. CancelFunc is a function that internally calls the cancel method of the cancelCtx object. The function of this method is to close the done channel of the cancelCtx object (representing the end of the context), and then cancel all the child contexts of the context, and cut off the context if necessary. The relationship with its parent context (in fact, the child context of this context is associated with propagateCancel).
Take a look at the interior of propagateCancel:

func propagateCancel(parent Context, child canceler) {
   if parent.Done() == nil {//父context如果永远不能取消,直接返回,不用关联。
      return
   }
   if p, ok := parentCancelCtx(parent); ok {//因为传入的父context类型是Context接口,不一定是CancelCtx,所以如果要关联,则先判断类型
      p.mu.Lock()//加锁,保护字段children
      if p.err != nil {//说明父context已经结束了,也别做其他操作了,直接取消这个子context吧
         child.cancel(false, p.err)
      } else {//没有结束,就在父context里加上子context的联系,用来之后取消子context用
         if p.children == nil {
            p.children = make(map[canceler]struct{})
         }
         p.children[child] = struct{}{}
      }
      p.mu.Unlock()
   } else {//因为传入的父context类型不是CancelCtx,则不一定有children字段的,只能起一个协程来监听父context的Done,如果Done关闭了,就可以取消子context了。
      go func() {
         select {
         case <-parent.Done():
            child.cancel(false, parent.Err())
         case <-child.Done()://为了避免子context比父context先取消,造成这个监听协程泄露,这里加了这样一个case
         }
      }()
   }
}

Let's take a look at what exactly does c.cancel (true, Canceled) in WithCancel's return & c, func () {c.cancel (true, Canceled)}:
because c is of type cancelCtx, there is a method of cancel This method is actually a method of implementing the cancel interface. In the field children map [canceler] struct {} of the previous cancelCtx field, you can see that the key of this map is this interface.

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

This interface contains two methods. The implementation classes are * cancelCtx and timerCtx.
Look
to achieve cancelCtx, the role of this method is to turn off cancelCtx objects done channel (on behalf of the end of this context), then all sub-context cancel this context, if necessary, and cut off this relationship and its parent context of context (in fact, The child context of this context is associated with propagateCancel):

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
   if err == nil {//任何context关闭后,都需要一个错误来给字段err赋值来表明结束的原因,不传入err是不行的
      panic("context: internal error: missing cancel error")
   }
   c.mu.Lock()//加锁
   if c.err != nil {//如果错误已经有了,说明这个context已经结束了,就不用cancel了
      c.mu.Unlock()
      return // already canceled
   }
   c.err = err//赋值错误原因
   if c.done == nil {
      c.done = closedchan //这个字段延迟加载,closedchan是一个context包中的量,一个已经关闭的channel,所有context都复用这个关闭的channel
   } else {//当然如果已经加载了,则直接关闭。那是啥时候加载的呢?当然是在这个context还没结束的时候,有人调用了Done()方法,所以是延迟加载。
      close(c.done)
   }
   for child := range c.children {//结束所有子context。之前每次的propagateCancel总算派上用场了。
      child.cancel(false, err)
   }
   c.children = nil
   c.mu.Unlock()

   if removeFromParent {//如果需要的话,切断当前context和父context的联系。就是从父context的children  map里移除嘛。当然如果fucontext不是cancelCtx,就没事咯
      removeChild(c.Context, c)
   }
}

This introduces the source code, and it looks good.

WithDeadline

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
Copy the parent context and set the deadline to d. If the parent context cutoff time is less than d, then the parent context cutoff time is used.
When one of the following occurs, the context will be canceled:

  • Deadline
  • Return the cancel function to be called
  • The parent context Done channel is closed
    uses examples
func main() {
   d := time.Now().Add(50 * time.Millisecond)
   ctx, cancel := context.WithDeadline(context.Background(), d)

   //即使ctx已经设置了截止时间,会自动过期,但是最好还是在不需要的时候主动调用cancel函数
   defer cancel()

   select {
   case <-time.After(1 * time.Second):
      fmt.Println("overslept")
   case <-ctx.Done():     //这个会先到达
      fmt.Println(ctx.Err())
   }
}

Source code analysis

Take a look at the implementation of WithDeadline:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
   if cur, ok := parent.Deadline(); ok && cur.Before(d) {//如果当前的Deadline比父Deadline晚,则用父Deadline,直接WithCancel就好了,因为父Deadline到了结束了,这个context也就结束了。WithCancel是为了返回一个cancelfunc。
      return WithCancel(parent)
   }
   c := &timerCtx{//可以看到,timerCtx其实就是包装了一下newCancelCtx,newCancelCtx在前文已经介绍了,这里看上去就简单多了。
      cancelCtx: newCancelCtx(parent),
      deadline:  d,
   }
   propagateCancel(parent, c)//propagateCancel在前文介绍过了,这里就是传播cancel嘛。
   dur := time.Until(d)
   if dur <= 0 {//时间到了,就直接可以cancel了
      c.cancel(true, DeadlineExceeded)
      return c, func() { c.cancel(false, Canceled) }
   }
   c.mu.Lock()
   defer c.mu.Unlock()
   if c.err == nil {//如果还没取消,就设置个定时器,AfterFunc函数就是说,在时间dur之后,执行func,即cancel。
      c.timer = time.AfterFunc(dur, func() {
         c.cancel(true, DeadlineExceeded)
      })
   }
   return c, func() { c.cancel(true, Canceled) }
}

Understand the implementation of WithCancel, the source code of this WithCancel is still very simple.

WithTimeout

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
After a certain period of time out, automatically cancel the context and its child context.
In fact, WithDeadline is used, but the deadline is written at the current time + timeout

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

The usage example is the same

func main() {
   ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
   defer cancel()

   select {
   case <-time.After(1 * time.Second):
      fmt.Println("overslept")
   case <-ctx.Done():
      fmt.Println(ctx.Err())
   }

}

Source code analysis

Just look at the source code of WithDeadline.

WithValue

func WithValue(parent Context, key, val interface{}) Context
Copy the parent context, and set the key value in the context. In this way, data can be taken out of the context and used.
It should be noted that the value of the context is used to transfer the data of the requested dimension, not the optional parameters of the function.
The key used for transferring data should not be a string or any other built-in type of Go, but should use a user-defined type as the key. This will avoid conflicts.
The key must be comparable, meaning it can be used to determine whether it is the same key, that is, equal.
The static type of the exported context key should use pointers or Interface
usage examples

func main() {
   type favContextKey string //定义一个自定义类型作为key

   f := func(ctx context.Context, k interface{}) {//判断key是否存在以及值的函数
      if v := ctx.Value(k); v != nil {
         fmt.Println("found value:", v)
         return
      }
      fmt.Println("key not found:", k)
   }

   k := favContextKey("language")
   ctx := context.WithValue(context.Background(), k, "Go") //使用自定义类型作为key

   f(ctx, k) //found value: Go
   f(ctx, favContextKey("color")) //key not found: color

   ctx2 := context.WithValue(ctx, "language", "Java") //使用string,试图覆盖之前的key对应的值
   f(ctx2, k) //found value: Go ,并没有被覆盖
   f(ctx2, "language") //found value: Java ,两个key互相独立
}

When using context to store key-value, the best way is not to export the key, the key is only accessible in the package, defined in the package, and then the package provides a safe access method to save the key-value and take the key- value. Such as:

type User struct {// User是我们要作为value保存在contex里的值
   //自定义字段
}

type key int //key是我们定义在包内的key类型,这样不会与其他包的冲突

var userKey key//key类型的变量,用作context里的key。

// NewContext方法用来将value存入context
func NewContext(ctx context.Context, u *User) context.Context {
   return context.WithValue(ctx, userKey, u)
}
// FromContext用来取value
func FromContext(ctx context.Context) (*User, bool) {
   u, ok := ctx.Value(userKey).(*User)
   return u, ok
}

Source code analysis

Take a look at the implementation of WithValue:

func WithValue(parent Context, key, val interface{}) Context {
   if key == nil {//没有key肯定是不行的辣
      panic("nil key")
   }
   if !reflectlite.TypeOf(key).Comparable() {//key不可比较也是不行的辣,Comparable()是接口Type的一个方法,不可比较,那么取value的时候,咋知道你到底想取啥
      panic("key is not comparable")
   }
   return &valueCtx{parent, key, val}
}

Seeing that a valueCtx object is returned, what is this, Kang Kang:

type valueCtx struct {
   Context
   key, val interface{}
}

Good guy, it turned out to be a context, plus two fields key and value. This is okay, then if there are more WithValue several times, then it must not be a long chain. It can be seen that every WithValue here, there is one more node. This chain can be long enough. Taking the value is to traverse the context chain upwards. Let's implement the Value () method of Kangkang. In fact, we know that Value (key interface {}) interface {} is a method of the context interface, so we look at this implementation in the valueCtx structure:

func (c *valueCtx) Value(key interface{}) interface{} {
   if c.key == key {//key的可比性就是用在这里的
      return c.val
   }
   return c.Context.Value(key)//如果当前valueCtx里没有找到这个key,就向上遍历链,直到找到为止
}

If the chain has been traversed upwards, if it can't find the key, it will terminate in the top context: background or todo to
see it:

var (
   background = new(emptyCtx)
   todo       = new(emptyCtx)
)
func Background() Context {
   return background
}
func TODO() Context {
   return todo
}

Yes, context.Background () and context.TODO () create such a thing, an object of type emptyCtx, what is emptyCtx, to intercept a small part of Kang Kang:

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 good guy is a terrifying hoax, essentially an int, and actually implements the context interface.
Its Value returns nil, and the case is solved.

Background

func Background() Context
Returns a non-nil and non-empty context that will never be cancelled, has no value, and has no deadline. Generally used in the main function, or initialization, used as the top-level context.

ALL

func TODO() Context
Returns a non-nil non-empty context. You can use this when you don't know what context to use, or if you don't use context yet but have this input parameter.

Guess you like

Origin www.cnblogs.com/chaunceeeeey/p/12740325.html