上下文 context 包简介
上下文context.Context
是用来设置终止时间、同步信号、传递请求相关的值的接口,与 Goroutine 关联密切。
context.Context
接口需要实现四个方法:
Deadline
:返回context.Context
被终止的时间,即完成任务的最终时限Done
:返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用Done
方法会返回同一个 ChannelErr
:返回context.Context
的结束原因,它只会有Done
方法对应的 Channel 关闭时才返回非空的值- 如果
context.Context
被动取消,返回Canceled
错误 - 如果
context.Context
超时,返回DeadlineExceeded
错误
- 如果
Value
:从context.Context
中获取键对应的值,对于同一个上下文来说,多次调用Value
并传入相同的Key
会返回相同的结果,该方法可以用来传递请求特定的数据
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
一、设计原理
在 Goroutine 构成的树形结构中对信息进行同步以减少计算资源的浪费是context.Context
的最大作用。Go 服务的每一个请求都是通过单独的 Goroutine 处理的,HTTP/RPC 请求的处理器会启动新的 Goroutine 访问数据库和其他服务。
一次请求可能需要创建多个 Goroutine 来处理,而context.Context
的作用是在不同的 Goroutine 之间同步特定数据、取消信号以及处理请求的最终时限。
每一个context.Context
都会从最顶层的 Goroutine 一层一层传递到最下层。context.Context
可以在上层 Goroutine 执行出现错误时,将信号及时同步给下层。
如果没有这个信息传递,当最上层的 Goroutine 执行失败时,下层的 Goroutine 会继续占用资源。context.Context
就可以通过信号及时停掉下层无用的工作以减少额外资源消耗。
1 简单示例
创建一个过期时间为 1s 的上下文,并向上下文传入handle
函数,该方法会使用 500ms 的时间处理传入的请求:
func handle(ctx context.Context, duration time.Duration) {
select {
case <-time.After(duration):
fmt.Println("process request with", duration)
case <-ctx.Done():
fmt.Println("handle", ctx.Err())
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
go handle(ctx, 1100*time.Millisecond)
select {
case <-ctx.Done():
fmt.Println("main", ctx.Err())
}
start:
goto start
}
因为过期时间大于处理时间,所以我们有足够的时间处理该请求,上述代码的输出结果:
$ go run main.go
process request with 500ms
main context deadline exceeded
handle
函数没有进入select
的超时分支,但是main
函数的select
却会等待
context.Context
超时并打印“main context deadline exceeded”。
如果将处理请求的时间增加到 1500ms,整个程序都会因为上下文的过期而被中止。
$ go run main.go
main context deadline exceeded
handle context deadline exceeded
多个 Goroutine 同时订阅ctx.Done()
管道中的消息,一旦接收到取消信号就立刻停止当前正在执行的任务。
二、默认上下文
context
包中最常用的方法是context.Background
和context.TODO
,这两个方法都会返回预先初始化好的私有变量background
和todo
,它们会在同一个 Go 程序中被复用:
func Background() Context {
return background
}
func TODO() Context {
return todo
}
这两个私有变量都是通过new(emptyCtx)
语句初始化的,它们指向私有结构体context.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
}
context.emptyCtx
通过空方法实现了context.Context
接口中规定的四个方法,但它没有任何功能。
从源码上看,context.Background
和context.TODO
只是互为别名,没有太大的区别,只是在使用的语义上稍有不同:
context.Background
是上下文的默认值,所有其他的上下文都应该从它衍生出来context.TODO
应该仅在不确定应该使用哪种上下文时使用
多数情况下,如果当前函数没有上下文作为入参,应该使用context.Background
作为起始的上下文向下传递。
三、取消信号
context.WithCancel
函数能够从context.Context
中衍生出一个新的子上下文并返回用于取消该上下文的函数。一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的 Groutine 都会同步收到这一取消信号。
context.WithCancel
的函数实现:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
context.newCancelCtx
函数将传入的上下文包装成私有结构体context.cancelCtx
。
context.propagateCancel
函数的作用是当父上下被取消时,让子上下文也被取消。
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
if done == nil {
return // 父上下文没被取消时直接返回
}
select {
case <-done:
child.cancel(false, parent.Err()) // 父上下文已被取消
return
default:
}
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// 父上下文已经被取消
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
上述函数中,与父上下文相关的情况有三种:
- 当
parent.Done() == nil
,也就是parent
没有触发取消事件时,当前函数直接返回 - 当
child
的继承链包含以下可以取消的上下文时,会判断parent
是否已经触发了取消信号:- 如果
parent
已经被取消,child
会立刻被取消 - 如果
parent
没有被取消,child
会被加入parent
的children
列表中,等待parent
释放取消信号
- 如果
- 当
parent
是开发者自定义的实现了context.Context
接口的类型并在Done()
方法中返回了非空的管道时:- 运行一个新的 Goroutine 同时监听
parent.Done()
和child.Done()
两个 Channel - 在
parent.Done()
关闭时调用child.cancel
取消子上下文
- 运行一个新的 Goroutine 同时监听
取消信号还有两个函数context.WithDeadline
和context.WithTimeout
。context.WithDeadline
和context.WithTimeout
都能创建可以被取消的计时器上下文context.timerCtx
。
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) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
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) }
}
context.WithDeadline
在创建context.timerCtx
的过程中判断了父上下文的最终时限与当前时间,并通过time.AferFunc
创建定时器,当时间超过了最终时限后会调用context.timerCtx.cancel
同步取消信号。
context.timeCtx
内部不仅通过嵌入context.cancelCtx
结构体继承了相关的变量和方法,还通过持有的定时器timer
和最终时限deadline
实现定时取消的功能:
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()
}
context.timerCtx.cancel
方法不仅调用了context.cancelCtx.cancel
,还会停止持有的定时器以减少不必在的资源浪费。
四、传值
上下文还有一个功能——传值。
context.WithValue
能从父上下文中创建一个子上下文,传值的子上下文使用context.valueCtx
类型:
func WithValue(parent Context, key, val interface{}) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
context.valueCtx
结构体会将除了Value
之外的Err
、Deadline
等方法代理到父上下文中,它只会响应context.valueCtx.Value
方法,该方法的实现也很简单:
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)
}
如果context.valueCtx
中存储的键值对与context.valueCtx.Value
方法中传入的参数不匹配,就会从父上下文中查找该键对应的值到某个父上下文中返回nil
或者查到到对的应值。
五、小结
Go 语言中的context.Context
的主要作用还是在多个 Goroutine 组成的树中同步取消信号以减少对资源的消耗和占用,虽然它也有传值的功能,但是这个功能很少用到。
在使用传值功能时要非常谨慎,因为使用context.Context
传递参数请求的所有参数是一种非常差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。