golang的Context分析

1. 初识Context

Context在汉语中有上下文的意思,而golang则提供一种叫context的结构, 通过context可以方便的在API或者协程之间传递请求作用域中的上下文信息(基础公共参数, 信号,以及超时信息)。使用时,需要请求的发起方带上Context,请求的接收方接受Context。当一个Context的被取消后,它所有的继承者也都会被取消,实际使用过程中,当前端的请求因超时被取消后,后续的所有请求都会自动被取消掉,而不需要浪费资源做无用的处理。 通过context可以方便的控制请求流程。

下面是源码中Context的定义,可以看到他是一个Interface。 定义了四种方法

type Context interface {
    // Done returns a channel that is closed when this Context is canceled or times out.
    // Done 方法返回一个channel,当Context超时或者被取消,这个channel会关闭
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel is closed.
    // Err 方法返回Done channel关闭的原因
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    // Deadline 返回context被cancel的时间点
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    // Value 根据key获取Context中传递的值(没有返回nil)
    Value(key interface{}) interface{}
}

标准库中提供了WithCancel, WithDeadline, WithTimeout三种方法来生成新的Context, 生成的新的Context都会复制parent context中的内容,并且都包含一个Done Channel。 三种方法生成的Context不同点是关闭Done Channel的条件不一样(Done Chanel被关闭了,这个Context就被取消了)。

WithCancel :  WithCancel返回一个含有Done Channel的新的Context和一个CancelFunc,当父Context被取消或者CancelFunc方法被调用时,Done Channel会被Close。

WithDeadline : WithDeadline返回一个含有expires的CancelContext,当达到deadline指定的时间,或者父Context被取消,Done Channel会被Close

WithTimeout : WithTimeout底层是调用了WithDeadline实现的,当Context超时或者父Context被取消,DoneChannel会被Close

与以上三种Context不一样的是,golang中还提供了下面几种Context。

WithValue : WithValue方法生成的Context,需要传入一个key和value值,其主要功能不是用来控制 goroutine,而是为了协程之间传递一些请求作用域级别的参数(比喻说登录态,请求的traceID等信息)

Background和TODO : 这两种Context实现完全相同,仅仅是语义上的差异,他们都不会被取消,没有deadline,不含有value值。当写代码时还没有想好传什么样的Context,我们可以用context.TODO占位。 而context.Background一般用在根Context中,用来生成其他类型的Context。

2. 从源码分析Context的实现:

1. Background和TODO的实现对比 :     

// 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
    
var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

从源码处进行分析,Background和TODO都是一个emptyCtx类型的Context,emptyCtx 是一个 int类型(这里没有使用struct{},因为struct{}是一个空的指针,不会占用额外的内存,所有的struct{}的地址都是相同的,但是emptyCtx类型的变量需要有确切的地址,因此使用int做emptyCtx),emptyCtx没有Deadline,也不会被Cancel。 2. WithCancel,WithDeadline和WithTimeout实现上的区别 :      调用WithCancel时, 首先会创建一个 cancelCtx ,并且一直向上回溯parentContext的类型,如果能找到一个cancelCtx,就将当前生成的cancelCtx设置成parentContext的Children(这样context取消的时候,才能向子节点传递,并取消各子节点)。 如果在parentContext中没有找到cancelCtx,就在本地启动一个 goroutine 用来处理 Done Channel 上的Close事件

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

而调用WithDeadline,首页会生成一个 timerCtx,并将当前的timerCtx设置成有cancelCtx的父节点的Children。 同时会注册一个定时器,当定时器超时触发时,调用cancel方法。

c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
    c.timer = time.AfterFunc(dur, func() {
        c.cancel(true, DeadlineExceeded)
    })
}

WithTimeout则通过调用WithDeadline实现,不做过多介绍

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

WithValue比较简单,这里就不做解析了

发表于: 2019年11月2日 10时32分