通过context.Context
,我们可以在 goruntine 中传递上下文参数以及同步取消信号。例如在处理http请求或记录请求链路时,可通过 Context 在goruntine间传递信息。也可以通过Context的超时取消,实现graceful shutdown。本文从源码角度,分析context.Context
是如何实现上下文传递以及同步取消信号。
后遗症:具有网络请求的第三方库,如果无法传递Context,都不太想用,因为路径追踪中无法显示。
接口实现
context.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{} }
|
- Deadline:返回Context被取消的截止时间,如果第二个参数返回fasle,则说明未设置截止时间;
- Done:返回一个channel对象,Context被取消后,该channel会被close;
- Err:返回Context结束原因。当Done返回的channel未被取消时,返回nil,被取消则返回取消原因。例如Context因为超时关闭,返回
DeadlineExceeded
;
- Value:返回Context中保存的键值;
context.canceler
接口
1 2 3 4
| type canceler interface { cancel(removeFromParent bool, err error) Done() <-chan struct{} }
|
- cancel:取消当前Context;
- Done:与Context接口一致,返回一个channel对象
基本原理
context包中提供了两个常用方法,context.Background
和context.TODO
。
1 2 3 4 5 6 7 8 9 10 11 12
| var ( background = new(emptyCtx) todo = new(emptyCtx) )
func Background() Context { return background }
func TODO() Context { return todo }
|
从源码上看,两者并没什么不同,均通过new(emptyCtx)
初始化。使用场景上,context.Backgound
常用于主函数、初始化、以及测试用例中,作为顶层上下文进行传递,仅当不确定使用哪种Context时,才通过context.TODO
占坑。对应emptyCtx实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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 啥都没干,没有截止时间,不能被取消,也不能设置上下文参数,仅仅通过空方法实现了Context接口。如果需要实现传递上下文、同步取消信号等额外功能,可通过context包中提供的方法进行拓展,当然也可以自己实现Context接口。
WithValue
context.WithValue
基于parent Context
,生成valueCtx类型Context,并保留一对键值,常用来传递上下文。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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} }
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) }
|
需要注意的是,valueCtx.Value
实现了链式查找,如果当前context中为找到为符合的key值,则会向父context中继续。
举个例子
示例代码中,ctx获取到grandparent context的value。
1 2 3 4 5 6 7 8 9 10 11
| package main
import "context"
func main() { ctx := context.Background() ctx = context.WithValue(ctx, "key1", "1") ctx = context.WithValue(ctx, "key2", "2") ctx = context.WithValue(ctx, "key3", "3") println(ctx.Value("key1").(string)) }
|
WithCancel
context.WithCancel
常用于控制派生 goruntine,通过接收parent Context
,返回两个参数ctx cancelCtx
和cancel cancelFunc
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| type CancelFunc func()
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) } }
func newCancelCtx(parent Context) cancelCtx { return cancelCtx{Context: parent} }
type cancelCtx struct { Context
mu sync.Mutex done chan struct{} children map[canceler]struct{} err error }
|
这里主要看下propagateCancel
的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
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 { go func() { select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): } }() } }
|
通过源码,propagateCancel
函数主要作用就是关联当前context和父context,如果父context是cancelCtx类型并且未被cancel,则加入到它的children字段中。当执行 cancel 函数时,除了关闭自身done channel
外,还会为 children 中关联的所有context执行 cancel 方法。通过Err方法,可获取context.Canceled
错误。cancelCtx.cancel
实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| var Canceled = errors.New("context canceled")
func (c *cancelCtx) cancel(removeFromParent bool, err error) { if err == nil { panic("context: internal error: missing cancel 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) } }
|
举个例子
示例代码,sleep 3秒后,因为执行 cancel,ctx.Err()输出context canceled
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| package main
import ( "context" "fmt" "time" )
func main() { ctx := context.Background() ctx, cancel := context.WithCancel(ctx) go f(ctx) time.Sleep(time.Second * 3) cancel() time.Sleep(time.Second * 1) }
func f(ctx context.Context) { i := 1 for { select { case <-ctx.Done(): fmt.Println(ctx.Err()) return default: fmt.Println(i) time.Sleep(time.Second * 1) i++ } } }
|
WithDeadline
context.WithDeadline
常用于需要超时关闭的场景。通过设置截止时间,起到超时自动取消context以及子context的效果。当context因为超时被 cancel 时,通过Err方法,可获取context.DeadlineExceeded
错误,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 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) { 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) } }
|
举个例子
示例代码,执行3秒后,定时器执行 cancel,ctx.Err()输出context deadline exceeded
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| package main
import ( "context" "fmt" "time" )
func main() { ctx := context.Background() ctx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Second * 3)) defer cancel() go f(ctx) time.Sleep(time.Second * 5) }
func f(ctx context.Context) { i := 1 for { select { case <-ctx.Done(): fmt.Println(ctx.Err()) return default: fmt.Println(i) time.Sleep(time.Second * 1) i++ } } }
|
WithTimeout
与context.WithDeadline
函数类似,只不过传参从time.Time类型该为time.Duration,也就是截止时间改为超时时间。实际调用的其实还是context.WithDeadline
1 2 3
| func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) }
|