再也不怕面试之Context篇

1,044 阅读4分钟

基本介绍

  Go 语言中的 Context 的主要作用还是在多个 Goroutine 或者模块之间同步取消信号或者截止日期,用于减少对资源的消耗和长时间占用,避免资源浪。
  在Golang中有三种并发控制方式:waitGroupContextchannel
  我们知道,waitGroup适用于多goroutine执行一个任务的场景,channel可以用于并发协程度优雅退出,但是若有多个goroutine都需要退出呢?如果这些 goroutine 又衍生了其它更多的goroutine呢,此时再使用channel就不礼貌了,这就引出了本文的主角:Context

一个接口

type Context interface {
    

    //Deadline返回代表此上下文完成的工作应被取消的时间。
    //未设置截止日期时,Deadline返回ok==false。连续调用Deadline返回相同的结果
    Deadline() (deadline time.Time, ok bool)
    
   // Done 方法需要返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消之后关闭.
    //如果context不能被取消,Done可能返回nil。对Done的连续调用将返回相同的值

    //Done 用于select语句中
    Done() <-chan struct{}

	//在Done关闭后,Err返回一个非空的错误值
    //Err 方法会返回当前 Context 结束的原因,它只会在 Done 返回的 Channel 被关闭时才会返回非空的值;
    	// 如果当前 Context 被取消就会返回 Canceled 错误;
    	//如果当前 Context 超时就会返回 DeadlineExceeded 错误;
    Err() error

   //Value 方法会从 Context 中返回键对应的值,key没有对应的值,则返回nil
    // 对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,这个功能可以用来传递请求特定的数据,
    //一般用全局变量声明键值
    Value(key interface{}) interface{}
    
}

在Golang标准库的Context包中,有这样一句话,不要在结构类型中存储上下文;相反,应该将Context显式地传递给需要它的每个函数。Context应该是第一个参数,通常命名为ctx。

四个实现

emptyCtx

//emptyCtx永远不会取消,没有值,也没有截止日期,它不是结构体
//因为这种类型变量必须要有不同的地址
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
}

BackGround和TODO

一般我们不直接使用emptyCtx,而是使用emptyCtx实例化的两个变量。

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

func Background() Context {
   return background
}

func TODO() Context {
   return todo
}

Background通常被用于主函数、初始化以及测试中,作为一个顶层的contextTODO通常是在不确定Context什么时候会被使用的时候才会被使用

cancelCtx

除了emptyContextvalueContextcancelCtxtimerCtx还实现了下面这个接口:

//canceler是一种可以直接取消的上下文类型,cacelCtx和timerCtx都实现了这个接口
type canceler interface {
    //为true时将当前context从父元素中移除
   cancel(removeFromParent bool, err error)
    //退出通道
   Done() <-chan struct{}
}

这里展示cancelCtx结构体和它实现的``canceler接口中的方法:Donecancel`

type cancelCtx struct {
   Context

   mu       sync.Mutex            // protects following fields
  //用来传递关闭信号
   done     atomic.Value          
   //存放当前路径
   children map[canceler]struct{} 
   //存储错误信息
   err      error   

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

//removeFromParent:为true时将c从其父元素移除
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)
   }
}

  Done方法很简单,判断cancelCtx.done参数是否为nil,如果不是空,则初始化一个chan struct{}并返回,也就是说cancelCtx是用channel来传递关闭信号。
  cancel方法调用时会设置取消的原因,同时还会将关闭通道done关闭,然后将子结点的context依次取消。

context.WithCancel()

  我们再来看context.WithCancel内部的实现,该方法返回一个ContextCancelFunc

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    //将传入的Context包装为cancelContext
	c := newCancelCtx(parent)
    //会构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消:
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}
/*
propagateCancel的作用是在 parent 和 child 之间同步取消和结束的信号,保证在 parent 被取消时,child 也会收到对应的信号,不会出现状态不一致的情况。
下面函数与父上下文有三种不同的情况
	1. 父上下文的推出通道,也就是done是nil,说明没有触发取消
	2.当 child 的继承链包含可以取消的上下文时,会判断 parent 是否已经触发了取消信号
		1.如果被取消,child会立即取消
		2.没有被取消,child 会被加入 parent 的 children 列表中,等待 parent 释放取消信号;
	3.当父上下文是开发者自定义的类型、实现了 context.Context 接口并在 Done() 方法中返回了非空的管道时
		1.运行一个新的 Goroutine 同时监听 parent.Done() 和 child.Done() 两个 Channel;
		2.在 parent.Done() 关闭时调用 child.cancel 取消子上下文;
*/
func propagateCancel(parent Context, child canceler) {
	done := parent.Done()
    //1.
	if done == nil {
		return // 父上下文不会触发取消信号
	}
	select {
	case <-done:
		child.cancel(false, parent.Err()) // 父上下文已经被取消
		return
	default:
	}
    //2.
	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			child.cancel(false, p.err)
		} else {
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
        //3.
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}
//判断parent是否已经触发了取消信号以及判断是否是cancelContext
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
	done := parent.Done()
    //closechan :可重复使用的关闭通道
	if done == closedchan || done == nil {
		return nil, false
	}
    //类型断言 判断是cancelCtx还是用户自定义的Context
    //&cancelCtxKey:cancelCtx返回自身的键  var cancelCtxKey int
	p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
	if !ok {
		return nil, false
	}
    //判断父上下文是否取消
	pdone, _ := p.done.Load().(chan struct{})
	if pdone != done {
		return nil, false
	}
	return p, true
}
// CancelFunc命令一个操作中止它的工作
// CancelFunc不会等待工作停止
// CancelFunc可以被多个goroutine同时调用。
// 在第一次调用之后,对CancelFunc的后续调用什么也不做
type CancelFunc func()

timerCtx

下面是timerCtx的结构体,该方法也实现了前面的canceler接口

type timerCtx struct {
   cancelCtx
   //定时器 在cancelCtx.mu的保护下,保证线程安全
   timer *time.Timer 
   //截止时间
   deadline time.Time
}

  该实现基于cancelCtx既可以根据需求主要取消,也可以到达deadline时自动取消。

context.WithDeadline() 和 context.WithTimeout()

  下面将要介绍的WithDeadLineWithTimeOut方法都会创建timerCtx,区别在于WithDeadLine必须指定一个时间点,而WithTimeOut可以接受一个时间段。

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) {
		// 当前的deadline已经比新的deadline早了.
		return WithCancel(parent)
	}
    // 设置timerCtx的deadline
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
    //这里不再介绍
	propagateCancel(parent, c)
    //它是time.Sub(time.Now())的缩写
	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) }
}

valueCtx

valueCtx,看到名字就知道用途了,内部的实现相比前面的也很简单

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

/*
如果当前 valueCtx 中存储的键与 Value 方法中传入的不匹配,
就会从父上下文中查找该键对应的值,直到在某个父上下文中返回 nil 或者查找到对应的值
*/
func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

  比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。

context.WithValue()

  context.WithValue内部使用的就是context.valueCtx,至此,六个方法都介绍完了。

func WithValue(parent Context, key, val interface{}) Context {
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

  context.valueCtx结构体会将除了 Value 之外的 ErrDeadline 等方法代理到父上下文中,它只会响应 context.valueCtx.Value 方法。
  至此我们已经将一个接口、四种实现、六个函数介绍完毕。面试的时候也就不怕被问到Context相关的内容了。

context使用原则

  • 不要把Context放在结构体中,要以参数的方式传递。
  • 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
  • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO。
  • Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递。
  • Context是线程安全的,可以放心的在多个goroutine中传递。

参考:
幼麟研究室——Context了解一下~
Go语言设计与实现——上下文Context