Go 上下文Context介绍

148 阅读1分钟

介绍

在go服务中,每个请求进来都会在自己的goroutine中处理。请求处理程序经常会启动额外的goroutine来访问数据库或rpc服务。这些goroutine处理请求需要用到一些特殊的,例如终端用户身份,授权令牌,请求截止时间。当一个请求被取消或者超时,所有的goroutines应该快速退出,以便系统可以回收他们正在使用的资源。

所以go 在标准库 context 定义了一个上下文context类型来完成上述功能,该类型贯穿了api的边界和进程之间携带截止时间,取消信号量和其他请求范围内的值。

Context 接口

type Context interface {
   //获取当前 context 的截止时间,
   Deadline() (deadline time.Time, ok bool)
   //获取一个只读的 channel,用于识别当前 channel 是否已经被关闭
   Done() <-chan struct{}
   //获取当前 context 被关闭的原因(超时或cancel),没有被关闭返回nil
   Err() error
   //获取当前 context 存储的数据
   Value(key interface{}) interface{}
}

Context 类型

官方context类型,主要有四种,分别是

  • emptyCtx 空context,实现了context接口,但每个方法返回的都是nil
  • cancelCtx 用于取消事件的context
  • timerCtx 用于超时通知的context
  • valueCtx 用于传递上下文信息

emptyCtx

常用context.Background() 或 context.TODO() 方法返回一个emptyCtx。 两种emptyCtx区别如注释所说,其源码实现是一样的,只不过使用场景不同,context.Background()通常由主函数、初始化和测试使用,是顶级Context;context.TODO()通常用于主协程外的其他协程向下传递,分析工具可识别它在调用栈中传播。 源码如下:

var (
   background = new(emptyCtx)
   todo       = new(emptyCtx)
)


// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
   return background
}

// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
   return todo
}

emptyCtx 实际表示是空context,所以他实现context接口方法返回的都是nil emptyCtx实现context接口:

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 any) any {
   return nil
}

cancelCtx

context.WithCancel方法会返回cancelCtx和返回用于取消cancelCtx的函数(CancelFunc)。 源码如下:

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

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

propagateCancel方法

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()
      if p.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() {
         select {
         case <-parent.Done():
            child.cancel(false, parent.Err())
         case <-child.Done():
         }
      }()
   }
}

propagateCancel方法

  1. 首先会调用parent.Done(),如果为nil 则说明parent是 emptyCtx从无cancel,直接return
  2. 否则会判断是否parent已经cancel ,若是则会调用当前ctx进行cancel并return
  3. parentCancelCtx方法找到父类是cancelCtx的,并将当前context加入到父类的children中,用于父类取消事件时一同被通知
  4. 如果parentCancelCtx方法未被找到则会启动一个新goroutine去监听parent或child的取消事件通知

cancelCtx

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
}

cancelCtx结构体属性字段

  • mu 用于并发控制
  • done作为取消信息
  • children用于记录该conetxt对应的所有cancelCtx子集
  • err当被cancel时写入错误信息

cancelFunc函数

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
   if err == nil {
      panic("context: internal error: missing cancel error")
   }
   c.mu.Lock()
   if c.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)
   }
}

从代码中看出cancel主要作用就是将cancelCtx 的done属性字段进行赋值closedchan或关闭,err属性字段赋错误信息,然后遍历其子集逐一cancel最后删除子集。

未命名文件.png

其他coontext接口函数实现如下:

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

func (c *cancelCtx) Done() <-chan struct{} {
   d := c.done.Load()
   if d != nil {
      return d.(chan struct{})
   }
   c.mu.Lock()
   defer c.mu.Unlock()
   d = c.done.Load()
   if d == nil {
      d = make(chan struct{})
      c.done.Store(d)
   }
   return d.(chan struct{})
}

func (c *cancelCtx) Err() error {`
   c.mu.Lock()
   err := c.err
   c.mu.Unlock()
   return err
}

示例:

func main() {
   ctxParent, cancel := context.WithCancel(context.Background())
   ctxChild, _ := context.WithCancel(ctxParent)
   // parent ctx cancel
   cancel()
   go func() {
      select {
      case <-ctxChild.Done():
         fmt.Println( "child ctx cancel")
      }
   }()
   select {
   case <-ctxParent.Done():
      fmt.Println("parent ctx cancel")
   }
   time.Sleep(time.Second)
}

//output:
//parent ctx cancel
//child ctx cancel

timerCtx

由context.WithTimeout函数返回,其主要作用是timeout和deadline事件,源码如下:


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

   deadline time.Time
}


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


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

WithDeadline方法会首先判断当前时间已经过了截止日期,如果过了返回的是一个cancelCtx。 否则会创建一个timerCtx,timerCtx携带一个timer和一个deadline。 它嵌入了一个cancelCtx 到来实现 context接口方法。timerCtx通过time.AfterFunc定时调用 cancel 方法。

valueCtx

由context.WithValue 方法返回,其作用是向上下文传递key-value。 源码如下:


type any = interface{}

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}
}

从结构体发现valueCtx其实就是携带了一个key和value字段,那么valueCtx是如何通过key查询到value呢?

Value方法源码:

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

SoWkIImgAStDuKhEpoj9hIY9JrJGqjLLWF6Ik90ah1IoKojH-6M928r9G3vS3Y6AfYi9gWiXW69VCXUSXMHS4a8KpRXag74EgNafG5y00000.png

valueCtx 类型会先查找自身一层的val是否存在,没有则会从父类开始向上逐层查询。

总结

context经常作为一个方法的第一参数,以便向整个链路传递上下文信息如超时时间、特殊值和取消通知。 在context使用过程中并发场景下使用cancelCtx或timerCtx时要考虑是否会出现其中一个goroutine被cancel后其他goroutine也要一起cancel。如果不需要应该为每个goroutine重新建立单独的cancelCtx或timerCtx,以免影响其他goroutine。

参考: