简介

Context: 上下文,上下层间传递的内容,通常理解为一个程序单元(goroutine)的运行状态、快照或者现场。主要应用在由一个请求衍生出的多个goroutine间需要满足一定的约束关系,实现诸如有效期,终止线程树,传递请求的全局变量等。

约定:方法第一个参数是 context.Context的变量。

使用:导入Context包,在每个资源方法中调用它,然后在使用时检查Context是否被取消,如果已经被Cancel,则释放绑定的资源。

核心接口

Context

  • 接口定义

    1
    2
    3
    4
    5
    6
    
    type Context interface{
        Deadline() (deadline time.Time, ok bool)
        Done() <- chan struct{}
        Err() error
        Value(key interface{}) interface{}
    }
    • 默认错误
    1
    2
    
    var Canceled = errors.New("context canceled")
    var DeadlineExceeded = errors.New("context deadline exceeded")
  • Context解读

    • context包里的方法是线程安全的,可被多个线程使用,但要注意使用这些数据时需同步,比如返回一个map,则map的读写要加锁。
    • Context被canceled or timeout, Done返回一个被closed的channel
    • Done的channel被closed后,Err表示关闭的原因
    • 如果Context未被关闭,Deadline返回Context将要关闭的时间
    • 如果Context未被关闭,Value返回与Key关联好的值,不存在返回nil。
  • Context 提供两个空方法

    Background() 和 TODO()都返回一个空的Context实例

    • context.Background(): 返回一个空的Context, 它不能被取消、没有值、也没有过期时间。常常作为处理Request的顶层context存在。
    • context.TODO(): 用在还不清楚要使用的上下文或尚不可用时。

Context使用

无论是那个Goroutine,他们的创建和调用关系总像是层层调用进行的,就像人的辈分一样,而更靠顶部的Goroutine应有办法主动关闭其下属的Goroutine的执行(不然程序可能面临失控的风险)。为了实现这种关系,Context结构也应该像一棵树,叶子结点须总是由根节点衍生而得到。

  • 创建Context树,即首先要得到根节点,context.Background()返回根节点。

    1
    
    func Background() Context
    • 有了根节点,又该如何创建其子节点…, 孙节点…? context包提供有一下函数供我们调用

      1
      2
      3
      4
      
      func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
      func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
      func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
      func WithValue(parent Context, key interface{}, val interface{}) Context

      函数都接受一个Context类型的参数parent, 并返回一个Context类型的值,这样就层层创建出不同的节点。子节点是从复制父节点得到的,并且根据接受参数设定子节点的一些状态值,接着就可以将子节点传递给下层Goroutine。

      • WithCancel(): 将父节点复制到子节点, 返回cancelCtx和一个CancelFunc函数类型的变量,其定义为:type CancelFunc func(). 调用这个CancelFunc时,撤销对应的Context对象,也就是让他的后代goroutine退出。代码处理方式如下
      1
      2
      3
      4
      
      select {
      case <-cancelCtx.Done():
          // do some clean
      }
  • WithDeadline() & WithTimeout(): 返回timerCtx(parent的一个副本)和一个CancelFunc函数类型的变量。WithDeadline和WithTimeout相似,前者设置具体的deadline时间,到达deadline时后代goroutine退出;后者传入一个从现在开始Context剩余的生命时长。

  • withValue(): 返回valueCtx, WithValue是在Context中设置一个map,拿到这个Context以及他的后代的goroutine都可以调用Value(key)方法得到map的value值。注: key相同则前者会被后者覆盖掉。

小结

context包通过构建树型关系的Context,来达到上一层Goroutine能对传递给下一层Goroutine的控制。对于处理一个Request请求操作,需要采用context来层层控制Goroutine,以及传递一些变量来共享。

  • Context对象的生存周期一般仅为一个请求的处理周期。即针对一个请求创建一个Context变量(它为Context树结构的根);在请求处理结束后,撤销此ctx变量,释放资源。

  • 每次创建一个Goroutine,要么将原有的Context传递给Goroutine,要么创建一个子Context并传递给Goroutine。

  • Context能灵活地存储不同类型、不同数目的值,并且使多个Goroutine安全地读写其中的值。

  • 当通过父Context对象创建子Context对象时,可同时获得子Context的一个撤销函数,这样父Context对象的创建环境就获得了对子Context将要被传递到的Goroutine的撤销权。

  • 在子Context被传递到的goroutine中,应该对该子Context的Done信道(channel)进行监控,一旦该信道被关闭(即上层运行环境撤销了本goroutine的执行),应主动终止对当前请求信息的处理,释放资源并返回。

使用原则

使用Context的程序包需要遵循如下的原则来满足接口的一致性以及便于静态分析。

  • 不要把Context存在一个结构体当中,显式地传入函数。Context变量需要作为第一个参数使用,一般命名为ctx;
  • 即使方法允许,也不要传入一个nil的Context,如果你不确定你要用什么Context的时候传一个context.TODO;
  • 使用context的Value相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数;
  • 同样的Context可以用来传递到不同的goroutine中,Context在多个goroutine中是安全的;

See Also

Thanks to the authors 🙂