Golang context 使用及源码解析 context 是 go 语言中并发控制的一大利器,在复杂的 goroutine 场景中,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 27 28 func main () { var ctx, cancel = context.WithCancel(context.Background()) go func () { bar(ctx, "韭菜" ) fmt.Println("韭菜 end" ) }() go func () { bar(ctx, "鸡腿" ) fmt.Println("鸡腿 end" ) }() time.Sleep(3 * time.Second) cancel() time.Sleep(time.Second) fmt.Println("all end" ) } func bar (ctx context.Context, food string ) { for i := 1 ; ; i++ { select { case <-ctx.Done(): return default : fmt.Printf("bar %s now\n" , food) time.Sleep(time.Second) } } }
输出:
1 2 3 4 5 6 7 8 9 bar 韭菜 now bar 鸡腿 now bar 鸡腿 now bar 韭菜 now bar 韭菜 now bar 鸡腿 now 鸡腿 end 韭菜 end all end
源码解析 数据结构 Context 接口
1 2 3 4 5 6 7 8 9 type Context interface { Deadline() (deadline time.Time, ok bool ) Done() <-chan struct {} Err() error Value(key any) any }
Context 是一个接口,包含四种方法:
Deadline,返回一个时间和布尔值,表示这个 ctx 的超时时间以及是否超时
Done,是一个 channel 类型,ctx 调用 cancel 函数,会向这个管道发送数据
Err,一些可能的错误
Value,context 以键值对的方式存储一些数据
接口实现 实现了上述接口的结构体有四个。
emptyCtx 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 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 any) any { return nil } func (e *emptyCtx) String() string { switch e { case background: return "context.Background" case todo: return "context.TODO" } return "unknown empty Context" }
emptyCtx 的实现很简单,它主要是用于 context 的初始化,当我们在调用 Background 或 TODO 时,其实就是初始化一个 emptyCtx。
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 }
cancelCtx 在应用中,我们会调用 ctx.WithContext
这个函数,该函数会返回一个 context 和一个 cancelFunc。
1 2 3 4 func WithCancel (parent Context) (ctx Context, cancel CancelFunc) { c := withCancel(parent) return c, func () { c.cancel(true , Canceled, nil ) } }
一句一句看。
可以看到先是调用了 withCancel
函数。
1 2 3 4 5 6 7 8 func withCancel (parent Context) *cancelCtx { if parent == nil { panic ("cannot create context from nil parent" ) } c := newCancelCtx(parent) propagateCancel(parent, c) return c }
其中,最需要关心的是两
newCancelCtx
1 2 3 func newCancelCtx (parent Context) *cancelCtx { return &cancelCtx{Context: parent} }
该函数返回一个 cancelCtx 类型,并将父 context 挂上,形成一个 context 链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type cancelCtx struct { Context mu sync.Mutex done atomic.Value children map [canceler]struct {} err error cause error } type canceler interface { cancel(removeFromParent bool , err, cause error ) Done() <-chan struct {} }
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 func propagateCancel (parent Context, child canceler) { done := parent.Done() if done == nil { return } select { case <-done: child.cancel(false , parent.Err(), Cause(parent)) return default : } if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() if p.err != nil { child.cancel(false , p.err, p.cause) } else { if p.children == nil { p.children = make (map [canceler]struct {}) } p.children[child] = struct {}{} } p.mu.Unlock() } else { goroutines.Add(1 ) go func () { select { case <-parent.Done(): child.cancel(false , parent.Err(), Cause(parent)) case <-child.Done(): } }() } } func parentCancelCtx (parent Context) (*cancelCtx, bool ) { done := parent.Done() if done == closedchan || done == nil { return nil , false } p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) if !ok { return nil , false } pdone, _ := p.done.Load().(chan struct {}) if pdone != done { return nil , false } return p, true }
初始化过程完了,那么看 WithCancel 的返回参数。一个是初始化好的 ctx,一个则是 cancel 方法。
1 return c, func () { c.cancel(true , Canceled, nil ) }
我们可以看到定义了一个函数
1 2 3 func () { c.cancel(true , Canceled, nil ) }
其中,Canceled
参数的定义是:
1 var Canceled = errors.New("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 32 33 34 35 36 37 38 func (c *cancelCtx) cancel(removeFromParent bool , err, cause error ) { if err == nil { panic ("context: internal error: missing cancel error" ) } if cause == nil { cause = err } c.mu.Lock() if c.err != nil { c.mu.Unlock() return } c.err = err c.cause = cause d, _ := c.done.Load().(chan struct {}) if d == nil { c.done.Store(closedchan) } else { close (d) } for child := range c.children { child.cancel(false , err, cause) } c.children = nil c.mu.Unlock() if removeFromParent { removeChild(c.Context, c) } }
以上就是 cancelCtx 的实现
valueCtx valueCtx 的实现很简单。
一个简单的使用 value 的示例是:
1 2 var ctx = context.WithValue(context.Background(), "hardews" , "https://hardews.cn/blog" )fmt.Println(ctx.Value("hardews" ))
查看源码可以看到 valueCtx 的定义:
1 2 3 4 type valueCtx struct { Context key, val any }
对于它的初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func WithValue (parent Context, key, val any) 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} }
对于它的一些方法:
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 func (c *valueCtx) String() string { return contextName(c.Context) + ".WithValue(type " + reflectlite.TypeOf(c.key).String() + ", val " + stringify(c.val) + ")" } func (c *valueCtx) Value(key any) any { if c.key == key { return c.val } return value(c.Context, key) } func value (c Context, key any) any { for { switch ctx := c.(type ) { case *valueCtx: if key == ctx.key { return ctx.val } c = ctx.Context case *cancelCtx: if key == &cancelCtxKey { return c } c = ctx.Context case *timerCtx: if key == &cancelCtxKey { return ctx.cancelCtx } c = ctx.Context case *emptyCtx: return nil default : return c.Value(key) } } }
timerCtx 直接切入正题
1 2 3 4 5 6 type timerCtx struct { *cancelCtx timer *time.Timer deadline time.Time }
当我们调用 WithTimeout 时,本质上都是调用 WithDeadline
1 2 3 func WithTimeout (parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) }
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 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, nil ) return c, func () { c.cancel(false , Canceled, nil ) } } c.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(dur, func () { c.cancel(true , DeadlineExceeded, nil ) }) } return c, func () { c.cancel(true , Canceled, nil ) } }
可以看到 timerCtx 的实现其实就是加个定时器,定时执行一个 cancel 函数。
Reference go语言标准库context.go源码解读 - 知乎 (zhihu.com)
go context 的源码解读 - 知乎 (zhihu.com)