文章目录

GoLang之Context包的用途是什么

1.Context包用途

在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。
在Google 内部,我们开发了 Context 包,专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。

2.context的数据结构

context的数据结构是:

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this `Context` is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this Context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}

3.Context中的方法

1.Done会返回一个channel,当该context被取消的时候,该channel会被关闭,同时对应的使用该context的routine也应该结束并返回。
2.Context中的方法是协程安全的,这也就代表了在父routine中创建的context,可以传递给任意数量的routine并让他们同时访问。
3.Deadline会返回一个超时时间,routine获得了超时时间后,可以对某些io操作设定超时时间。
4.Value可以让routine共享一些数据,当然获得数据是协程安全的。

这里需要注意一点的是在goroutine中使用context包的时候,通常我们需要在goroutine中新创建一个上下文的context,原因是:如果直接传递外部context到协层中,一个请求可能在主函数中已经结束,在goroutine中如果还没有结束的话,会直接导致goroutine中的运行的被取消.

go func() {
   _, ctx, _ := log.FromContextOrNew(context.Background(), nil)
}()

4.Context包的函数

4.1Background函数

context.Background函数的返回值是一个空的context,经常作为树的根结点,它一般由接收请求的第一个routine创建,不能被取消、没有值、也没有过期时间。
Background函数的声明如下:

// Background returns an empty Context. It is never canceled, has no deadline,
// and has no values. Background is typically used in main, init, and tests,
// and as the top-level `Context` for incoming requests.
func Background() Context

4.2WithCancel、WithTimeout函数

WithCancel 和 WithTimeout 函数 会返回继承的 Context 对象, 这些对象可以比它们的父 Context 更早地取消。
当请求处理函数返回时,与该请求关联的 Context 会被取消。
当使用多个副本发送请求时,可以使用 WithCancel取消多余的请求。
WithTimeout 在设置对后端服务器请求截止时间时非常有用。
下面是这三个函数的声明:

// WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// A CancelFunc cancels a Context.
type CancelFunc func()

// WithTimeout returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed, cancel is called, or timeout elapses. The new
// Context's Deadline is the sooner of now+timeout and the parent's deadline, if
// any. If the timer is still running, the cancel function releases its
// resources.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

调用CancelFunc对象将撤销对应的Context对象,这样父结点的所在的环境中,获得了撤销子节点context的权利,当触发某些条件时,可以调用CancelFunc对象来终止子结点树的所有routine。在子节点的routine中,需要判断何时退出routine:

select {
    case <-cxt.Done():
        // do some cleaning and return
}

根据cxt.Done()判断是否结束。当顶层的Request请求处理结束,或者外部取消了这次请求,就可以cancel掉顶层context,从而使整个请求的routine树得以退出。

4.5WithDeadline函数

WithDeadline和WithTimeout比WithCancel多了一个时间参数,它指示context存活的最长时间。如果超过了过期时间,会自动撤销它的子context。所以context的生命期是由父context的routine和deadline共同决定的。

4.4WithValue函数

WithValue 函数能够将请求作用域的数据与 Context 对象建立关系。
WithValue返回parent的一个副本,该副本保存了传入的key/value,而调用Context接口的Value(key)方法就可以得到val。注意在同一个context中设置key/value,若key相同,值会被覆盖。
Context上下文数据的存储就像一个树,每个结点只存储一个key/value对。WithValue()保存一个key/value对,它将父context嵌入到新的子context,并在节点中保存了key/value数据。Value()查询key对应的value数据,会从当前context中查询,如果查不到,会递归查询父context中的数据。
值得注意的是,context中的上下文数据并不是全局的,它只查询本节点及父节点们的数据,不能查询兄弟节点的数据。
声明如下:

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

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    ......
    return &valueCtx{parent, key, val}
}

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

5.Context的使用原则

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