深入解析go context 源码

1介绍及使用场景

相信大家都经历过面试官的灵魂拷问:

context 的作用是啥??

父context取消后,子context会立即取消吗?

今天让我们深入了解context,然后继续被面试官虐(* ̄︶ ̄)

context 是Go 1.7 引入标准库,被称作上下文,通常在goroutine间传递上下文,作用包括取消信号、超时控制、k-v 传递。k-v 传递通常用来做分布式链路追踪,上下文传值。

在一些需要超时控制的场景我们经常会看到它的身影,比如sql 里面,像ping 这样的场景,ping 什么时候结束?不结束肯定会一直等待着,那遇到异常场景就会一直阻塞了,显然是不行的。像http 请求也是需要超时控制,不控制,就会产生goroutine 泄露,不知道请求什么时候结束,那么请求就会一直等着。

 

// PingContext verifies a connection to the database is still alive,
// establishing a connection if necessary.
func (db *DB) PingContext(ctx context.Context) error {
    var dc *driverConn
    var err error
​
    for i := 0; i < maxBadConnRetries; i++ {
        dc, err = db.conn(ctx, cachedOrNewConn)
        if err != driver.ErrBadConn {
            break
        }
    }
    if err == driver.ErrBadConn {
        dc, err = db.conn(ctx, alwaysNewConn)
    }
    if err != nil {
        return err
    }
​
    return db.pingDC(ctx, dc, dc.releaseConn)
}

2 源码解析

结构

context 接口

context 携带截止日期,超时取消信号,和其他value,通过下面这些接口,下面这些接口方法会同时被多个goroutines 调用

// A Context carries a deadline, a cancellation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
   // Deadline returns the time when work done on behalf of this context
   // should be canceled. Deadline returns ok==false when no deadline is
   // set. Successive calls to Deadline return the same results.
   Deadline() (deadline time.Time, ok bool)
​
   // Done returns a channel that's closed when work done on behalf of this
   // context should be canceled. Done may return nil if this context can
   // never be canceled. Successive calls to Done return the same value.
   // The close of the Done channel may happen asynchronously,
   // after the cancel function returns.
   //
   // WithCancel arranges for Done to be closed when cancel is called;
   // WithDeadline arranges for Done to be closed when the deadline
   // expires; WithTimeout arranges for Done to be closed when the timeout
   // elapses.
   //
   // Done is provided for use in select statements:
   //
   //  // Stream generates values with DoSomething and sends them to out
   //  // until DoSomething returns an error or ctx.Done is closed.
   //  func Stream(ctx context.Context, out chan<- Value) error {
   //     for {
   //        v, err := DoSomething(ctx)
   //        if err != nil {
   //           return err
   //        }
   //        select {
   //        case <-ctx.Done():
   //           return ctx.Err()
   //        case out <- v:
   //        }
   //     }
   //  }
   //
   // See https://blog.golang.org/pipelines for more examples of how to use
   // a Done channel for cancellation.
   Done() <-chan struct{}
​
   // If Done is not yet closed, Err returns nil.
   // If Done is closed, Err returns a non-nil error explaining why:
   // Canceled if the context was canceled
   // or DeadlineExceeded if the context's deadline passed.
   // After Err returns a non-nil error, successive calls to Err return the same error.
   Err() error
​
   // Value returns the value associated with this context for key, or nil
   // if no value is associated with key. Successive calls to Value with
   // the same key returns the same result.
   //
   // Use context values only for request-scoped data that transits
   // processes and API boundaries, not for passing optional parameters to
   // functions.
   //
   // A key identifies a specific value in a Context. Functions that wish
   // to store values in Context typically allocate a key in a global
   // variable then use that key as the argument to context.WithValue and
   // Context.Value. A key can be any type that supports equality;
   // packages should define keys as an unexported type to avoid
   // collisions.
   //
   // Packages that define a Context key should provide type-safe accessors
   // for the values stored using that key:
   //
   //     // Package user defines a User type that's stored in Contexts.
   //     package user
   //
   //     import "context"
   //
   //     // User is the type of value stored in the Contexts.
   //     type User struct {...}
   //
   //     // key is an unexported type for keys defined in this package.
   //     // This prevents collisions with keys defined in other packages.
   //     type key int
   //
   //     // userKey is the key for user.User values in Contexts. It is
   //     // unexported; clients use user.NewContext and user.FromContext
   //     // instead of using this key directly.
   //     var userKey key
   //
   //     // NewContext returns a new Context that carries value u.
   //     func NewContext(ctx context.Context, u *User) context.Context {
   //        return context.WithValue(ctx, userKey, u)
   //     }
   //
   //     // FromContext returns the User value stored in ctx, if any.
   //     func FromContext(ctx context.Context) (*User, bool) {
   //        u, ok := ctx.Value(userKey).(*User)
   //        return u, ok
   //     }
   Value(key interface{}) interface{}
}

Deadline()

返回任务被取消时间,返回ok=false,当Deadline 没有设置的时候,比如下面的emptyCtx,除了timeCtx,其他接口都会选择继承父context 的 Deadline,当是emptyCtx 为nil,timeCtx为到期时间。

Done()

  • 返回一个channel,当任务被取消时会被关闭

  • 在cancel 方法返回时,channel 关闭可能是异步的

  • WithCancel 的作用是安排在调用cancel 时关闭Done()

  • WithDeadline的作用是安排在到达deadline 截止时期关闭Done()

  • Done() 通常配合select使用,如下面控制超时的例子

func Stream(ctx context.Context, out chan<- Value) error {
        for {
           v, err := DoSomething(ctx)
          if err != nil {
          return err
          }
          select {
           case <-ctx.Done(): //监控ctx.done 
              return ctx.Err()
          case out <- v:      //将值投入到out channel,如果channel 发送阻塞不能投递,就直接返回了
          }
        }
    }

Err() error

  • done 没有关闭,那么Err获取到的一定是nil

  • done 被关闭了,Err 返回的error 解释了为什么context 被取消了或者context's deadline 超时

  • 连续调用Err(),会返回同样的error

  • Value(key interface{}) interface{} 根据key 获取值,相当于一个key value 的存储,返回类型是接口类型

emptyCtx

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
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
}

实现了context接口,所有返回值都是空,有下面两个常用实现,这个context都不能被取消,也不能存储值,可以理解为最原始的根context

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
}

cancelCtx

// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
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
}
​
func (c *cancelCtx) Value(key interface{}) interface{} {
   if key == &cancelCtxKey {
      return c
   }
   return c.Context.Value(key)
}
​
//这里done 是赖生成的,如果调用Done,c.done 将不会nil
func (c *cancelCtx) Done() <-chan struct{} {
   c.mu.Lock()
   if c.done == nil {
      c.done = make(chan struct{})
   }
   d := c.done
   c.mu.Unlock()
   return d
}
​
func (c *cancelCtx) Err() error {
   c.mu.Lock()
   err := c.err
   c.mu.Unlock()
   return err
}
  • value方法将会在遇到cancelCtxKey将会返回自己

timerCtx

timerCtx携带了一个timer和自身携带deadline,继承了cancelCtx的Done 和 Err,实现了cancel 方法,在停止timer 的时候,调用cancelCtx.cancel取消

// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
   cancelCtx
   timer *time.Timer // Under cancelCtx.mu.
​
   deadline time.Time
}
​
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
   return c.deadline, true
}
​
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()
   if c.timer != nil {
      c.timer.Stop()
      c.timer = nil
   }
   c.mu.Unlock()
}

valueCtx

// 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{}
}
//
func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}
  • valueCtx 只实现了Value 方法,其他方法继承父Context,先从自己的key 进行查找,如果没找到,调用父Context的Value 方法继续往上找,整个过程相当于链表的遍历,直到找到值,或者遇到empty context返回nil

使用

WithCancel

// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
   if parent == nil {
      panic("cannot create context from nil parent")
   }
   c := newCancelCtx(parent) //创建一个CancelCtx
    propagateCancel(parent, &c)//繁殖Cancel,将parent 和child(c)进行关联
   return &c, func() { c.cancel(true, Canceled) }
}
  • WithCancel 返回parent 的拷贝,和一个cancel函数,当调用这个cancel,便会级联取消

newCancelCtx

返回初始化的cancelCtx,就是将cancelCtx的context 赋值为parent

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
   return cancelCtx{Context: parent}
}

propagateCancel

该函数作用就是将child 挂靠在parent 的children 集合上面,让parent 取消,child 也能取消,parent 必须是cancelctx ,因为只有挂靠在cancelctx才能达到级联取消效果

 

// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
   done := parent.Done() //获取  parent.done 
   if done == nil {//如果为nil,直接返回就行
      return // parent is never canceled
   }

   select {
   case <-done: //如果parent 已经被cancel 直接cancel child
      // parent is already canceled
      child.cancel(false, parent.Err())
      return
   default: //如果parent 没有被取消,则直接走下面默认逻辑
   }

   if p, ok := parentCancelCtx(parent); ok { //寻找最近的cancelctx
      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 { //来到这里的话说明是自定义cancelctx或者没有找到cancelctx
      atomic.AddInt32(&goroutines, +1)
      go func() {
         select {
         case <-parent.Done():
            child.cancel(false, parent.Err())
         case <-child.Done():
         }
      }()
   }
}
  • 先判断done == nil,如果成立直接返回,说明这时候为nil,查看上面结构,发现只有emptyctx done 返回nil,cancleCtx会返回make 后的的done,其他两个ctx 未实现继承传入的done()方法。

    如果来到这里说明,参数是context.TODO或者context.Background

  • 如果parent 已经被取消直接取消child 返回就行了

  • parentCancelCtx 从 父节点找到最近的一个cancleCtx,如果成功了的话走下面逻辑

    1、p.err != nil 代表cancleCtx被取消过了,所以应该马上取消child

    2、p.err = nil 将child放进cancleCtx的children集合里面

  • else 分支说明是自定义cancelctx,这时候为什么还要监听parent.Done()呢?

    parentCancelCtx返回false 有三种情况,一种是emptyctx或者该Context 被赋值为closedchan

    第二种是没有找到cancelCtxKey这种情况,第三种是done 不匹配,cancelCtx被继承了,但是返回了不一样的done

  • 下面最后一句go func()的作用:

    1、假如因为 parentCancelCtx中 done == closedchan || done == nil 返回,说明这个CancelCtx要么被取消了,要么父context没有CancelCtx,此时parent.Done()直接返回了,然后再取消child就行了。

    2、 p.done != done,当找到的父CancelCtx是自定义的时候,我们也要监听,因为没有信号通知child 取消

,所以当自定义的CancelCtx 取消的时候,也要监听这个,取消child。

为什么 还要 <- child.Done() ?假设child 取消了,parent 没有取消了,那这个协程就会一直阻塞在这,propagateCancel的作用就是关联父context 与child。所以在子child 取消的时候,我们应该直接返回就行了。

parentCancelCtx

// parentCancelCtx returns the underlying *cancelCtx for parent.
// It does this by looking up parent.Value(&cancelCtxKey) to find
// the innermost enclosing *cancelCtx and then checking whether
// parent.Done() matches that *cancelCtx. (If not, the *cancelCtx
// has been wrapped in a custom implementation providing a
// different done channel, in which case we should not bypass it.)
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
   done := parent.Done()
   if done == closedchan || done == nil {
      return nil, false
   }
   p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) //从parent查找cancelCtx
   if !ok {
      return nil, false
   }
   p.mu.Lock()
   ok = p.done == done //判断这两个done 是不是一样
   p.mu.Unlock()
   if !ok {
      return nil, false
   }
   return p, true
}
  • 如果parent.done 被关闭了或者是nil,直接返回false

    什么时候done == closedchan?

    答:在cancelCtx.cancel+464 行,如果c.done等于nil,c.done 会被赋值为closedchan

    什么时候done == nil?

    答:parent.Done()只有一种清空为nil,那就是context.TODO或者context.Background,因为cancelctx 在调用Done()时会make,不为nil,其他两种情况的ctx都是继承,没有自己实现done。

  • 从parent 里面查找cancelCtxKey所对应的cancelCtx,还记得上面cancelCtx Value方法在实现时,遇到cancelCtxKey会返回自己

  • 需要对返回的done 做个比较,判断p.done 是不是done ,如果不是这这个context是自定义包装过的,并且返回的是不同的done channel

cancel

// 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
   if c.done == nil {
      c.done = closedchan
   } else {
      close(c.done)
   }
   for child := range c.children { //循环遍历children 调用child.cancel
      // NOTE: acquiring the child's lock while holding parent's lock.
      child.cancel(false, err)
   }
   c.children = nil
   c.mu.Unlock()

    if removeFromParent { //如果removeFromParent ,将child 从parent(c.Context)里面移除
      removeChild(c.Context, c)
   }
}
  • 什么时候c.done == nil?

    cancelCtx.done不为nil只有一种情况,调用cancelCtx.Done(),如果像下面这种情况,没有人调用ctx.Done(),c.done 就为nil被赋值为closedchan

package main

import "context"

func main() {
	_,cancel:=context.WithCancel(context.Background())
	//go func() {
	//	select {
	//	case <-ctx.Done():
	//
	//	}
	//}()
	cancel()
}
  • cancel 关闭c.done,还记得我们调用case<-ctx.Done()这种情况,一旦c.done 被关闭,那么case就会被调用,到达退出协程的目的

  • 递归调用cancelCtx.children的cancel取消函数

cancel 流程

 

如果所示,举个例子来说明整个流程

情况1:A 被调用cancel或者A已经被调用cancel,这时候跟B的流程一样,这时候是找不到父cancelCtx ,在最后调用CancelFunc时,removeChild 直接返回了

情况2:B 被调用cancel,这时候,循环遍历B 的children,分别调用C,D的cancel,最后将自己的children 置为nil,最后根据removeFromParent选择是否从A 的children移除,在最后调用CancelFunc时,removeChild 就会从A 的children移除,所以注意不要忘记调用CancelFunc

情况3:D或C 被cancel,跟B 的流程一样,最后会从B 的children移除

removeChild

该函数作用是将context从父context移除,父context也必须是 cancelctx ,来看看我们前面的removeFromParent,当Withxx 返回CancelFunc调用时值是true,这时候调用context.cancel 会将自己从父context 移除,而当propagateCancel 时调用context.cancel 和遍历children 调用cancel 都传的false,这是因为这里不需要removeChild.

  • 在调用context.cancel时,会将该cancelctx的children 直接置空,所以不需要在去一个一个将该child从该cancelctx的children移除

  • 到最后调用CancelFunc的时候,需要直接将child 从父cancelctx的children 里面移除

// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
   p, ok := parentCancelCtx(parent) //找到最近一个cancelCtx 
   if !ok { //没找到,直接返回
      return
   }
   p.mu.Lock() //加锁操作
   if p.children != nil {//如果cancelctx.children不为nil,将child 从parent 里面删除
      delete(p.children, child)
   }
   p.mu.Unlock()
}

WithDeadline

// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
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{ //创建timerCtx
      cancelCtx: newCancelCtx(parent),
      deadline:  d,
   }
   propagateCancel(parent, c) //调用这个函数处理parent 和child 的关系
   dur := time.Until(d) //判断当前距离截止时间还有多少时间
   if dur <= 0 {//时间小于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.err 为nil,说明没有取消,则添加定时器到点取消
      c.timer = time.AfterFunc(dur, func() {
         c.cancel(true, DeadlineExceeded)
      })
   }
   return c, func() { c.cancel(true, Canceled) }
}

timerCtx. cancel

func (c *timerCtx) cancel(removeFromParent bool, err error) {
   c.cancelCtx.cancel(false, err) //调用cancelCtx.cancel
   if removeFromParent {
      // Remove this timerCtx from its parent cancelCtx's children.
      removeChild(c.cancelCtx.Context, c)
   }
   c.mu.Lock()
   if c.timer != nil {
      c.timer.Stop() //停止定时器,释放资源
      c.timer = nil
   }
   c.mu.Unlock()
}

WithTimeout

// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete:
//
//     func slowOperationWithTimeout(ctx context.Context) (Result, error) {
//        ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
//        defer cancel()  // releases resources if slowOperation completes before timeout elapses
//        return slowOperation(ctx)
//     }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
   return WithDeadline(parent, time.Now().Add(timeout))
}
  • 就是在WithDeadline基础加上了timeout,传进来的timeout是超时时间,代表距现在多少时间后超时,

    所有将现在时间+timeout就是什么时间点过期,直接调用WithDeadline就可以了

  • 调用CancelFunc 释放资源,所以在操作完成后,我们应该调用cancel 函数,通常使用defer

WithValue

// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The provided key must be comparable and should not be of type
// string or any other built-in type to avoid collisions between
// packages using context. Users of WithValue should define their own
// types for keys. To avoid allocating when assigning to an
// interface{}, context keys often have concrete type
// struct{}. Alternatively, exported context key variables' static
// type should be a pointer or interface.
func WithValue(parent Context, key, val interface{}) Context {
   if parent == nil { //parent 为空报错
      panic("cannot create context from nil parent")
   }
   if key == nil {//key 为空报错
      panic("nil key")
   }
    //通过反射key 判断该key 是否可比较,不可比较报错
   if !reflectlite.TypeOf(key).Comparable() {
      panic("key is not comparable")
   }
    //返回value context
   return &valueCtx{parent, key, val}
}
  • WithValue通过key 和value 返回parent 的拷贝,很好理解,因为创建了新的一个context继承parent,但是多了一个value

面试八股文

最后再来一个面试八股文

context 应用注意事项?

来自于go官方博客里,对的建议:

  1. Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx.

  2. Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.

  3. Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.

  4. The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.

中文译文

  • 不要将 Context 作为结构体字段。应该将 Context 类型作为函数的第一参数,并且通常命名为 ctx。看到过知名项目这样做过(* ̄︶ ̄)

  • 不要向函数传入一个 nil 的 context,不知道传什么就todo

  • 不应该将可选的函数参数进到 context ,context 应该存储的是一些有请求范围有共性的数据。例如: session、cookie ,traceId等。

  • 同一个 context 在多个 goroutine传递是并发安全的,因为有锁

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值