6.1 Context
context.Context
接口定义了四个方法:
Deadline
: 返回context.Context
被取消的时间,即完成工作的截止时间Done
:返回一个 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{}
}
6.1.1 设计原理
context.Context
最大作用是 在 Goroutine 构成的树形结构中对信号进行同步以减少计算资源的浪费。
context.Context
的作用是在不同 Goroutine 之间同步请求特定数据、取消信号以及处理请求的截止日期。
每一个 context.Context
都会从最顶层的 Goroutine 一层一层传递到最下层。context.Context
可以在上层 Goroutine 执行出现错误时,将信号及时同步给下层。
当最上层的 Goroutine 因为某些原因执行失败时,下层的 Goroutine 由于没有接收到这个信号所以会继续工作。
当使用 context.Context
时,就可以在下层及时停掉无用的工作以减少额外资源的消耗:
6.1.2 默认上下文
context.Background
、context.TODO
都会返回预先初始化好的私有变量 background
和 todo
:
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
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
作为起始的上下文向下传递。
6.1.3 取消信号
WithCancel
context.WithCancel
函数能够从 context.Context
中衍生出一个新的Context并返回用于取消该上下文的函数。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
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 {
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
函数流程如下:
parent.Done() == nil
:parent
不会触发取消事件时,当前函数会直接返回child
的继承链包含可以取消的上下文时,会判断parent
是否已经触发了取消信号:- 若已经被取消,
child
会立刻被取消 - 若没有被取消,
child
会被加入parent
的children
列表中,等待parent
释放取消信号
- 若已经被取消,
- 当父上下文是开发者自定义的类型、实现了
context.Context
接口并在Done()
方法中返回了非空的管道时:- 运行一个新的 Goroutine 同时监听
parent.Done()
和child.Done()
两个 Channel - 在
parent.Done()
关闭时调用child.cancel
取消子上下文
- 运行一个新的 Goroutine 同时监听
context.propagateCancel
在 parent
和 child
之间同步取消和结束的信号,保证在 parent
被取消时,child
也会收到对应的信号。
context.cancelCtx.cancel
会关闭上下文中的 Channel 并向所有的子上下文同步取消信号:
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return
}
c.err = err
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
for child := range c.children {
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
WithDeadline, 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 cur, ok := parent.Deadline(); ok && cur.Before(d) {
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // 已经过了截止日期
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) }
}
函数流程如下:
- 判断父上下文的截止日期与当前日期
- 使用
time.AfterFunc
创建定时器 - 时间超过了截止日期后调用
context.timerCtx.cancel
同步取消信号
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 {
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和定时器。
6.1.4 传值
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}
}
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
或者查找到对应的值。