Golang
中用于控制goroutine
的方式有很多,可以用channel
阻塞控制,但是更普遍的方式是使用context
上下文。
context
的功能主要有:
超时控制
截止时间控制(本质也是超时)
手动退出
传值
控制所有的子协程,一起退出
创建方式 context
在context
包中,是以interface
的形式出现的
1 2 3 4 5 6 type Context interface { Deadline() (deadline time.Time, ok bool ) Done() <-chan struct {} Err() error Value(key any) any }
创建ctx的方式以上面的几种功能为主
1 2 3 4 5 6 func Background () Context {} func TODO () Context {} func WithCancel (parent Context) (ctx Context, cancel CancelFunc) {} func WithDeadline (parent Context, d time.Time) (Context, CancelFunc) {} func WithTimeout (parent Context, timeout time.Duration) (Context, CancelFunc) {} func WithValue (parent Context, key, val any) Context {}
从创建方式可以看到,ctx
是从父ctx
衍生出来的子ctx
emptyCtx
空ctx
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 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" } var ( background = new (emptyCtx) todo = new (emptyCtx) )
方法中,全部返回nil
,因此,emptyCtx
其实是一个 “啥也不干” 的Context
;它通常用于创建 root Context,标准库中 context.Background()
和 context.TODO()
返回的就是这个 emptyCtx
。
1 2 3 4 5 6 7 func Background () Context { return background } func TODO () Context { return todo }
cancelCtx
带有取消功能的ctx
,可以取消ctx
。取消时,它还会取消所有的子ctx
。
1 2 3 4 5 6 7 8 type cancelCtx struct { Context mu sync.Mutex done atomic.Value children map [canceler]struct {} err error }
当ctx
从父ctx
中创建而来,Context
存储父ctx
。
当此ctx
创建子ctx
,则将子ctx
存储到children
中。取消时,便利children
中的子ctx,
逐个取消。
创建方式
1 2 3 4 5 6 7 8 9 10 11 12 13 func newCancelCtx (parent Context) cancelCtx { return cancelCtx{Context: parent} } 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) } }
包含的方法
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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 func (c *cancelCtx) Value(key any) any { if key == &cancelCtxKey { return c } return value(c.Context, key) } func (c *cancelCtx) Done() <-chan struct {} { d := c.done.Load() if d != nil { return d.(chan struct {}) } c.mu.Lock() defer c.mu.Unlock() d = c.done.Load() if d == nil { d = make (chan struct {}) c.done.Store(d) } return d.(chan struct {}) } func (c *cancelCtx) Err() error { c.mu.Lock() err := c.err c.mu.Unlock() return err } type stringer interface { String() string } func contextName (c Context) string { if s, ok := c.(stringer); ok { return s.String() } return reflectlite.TypeOf(c).String() } func (c *cancelCtx) String() string { return contextName(c.Context) + ".WithCancel" } 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 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) } c.children = nil c.mu.Unlock() if removeFromParent { removeChild(c.Context, c) } }
timerCtx
带有计时器和截止时间的ctx,可以在特定时间自动取消。通过*time.Timer
和time.Time
实现
1 2 3 4 5 6 type timerCtx struct { cancelCtx timer *time.Timer deadline time.Time }
创建方式
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 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) } } 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 func (c *timerCtx) Deadline() (deadline time.Time, ok bool ) { return c.deadline, true } func (c *timerCtx) String() string { return contextName(c.cancelCtx.Context) + ".WithDeadline(" + c.deadline.String() + " [" + time.Until(c.deadline).String() + "])" } 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() }
valueCtx
包含键值对的ctx,可以存储键值对,也可以读取键值对
数据结构
1 2 3 4 type valueCtx struct { Context key, val any }
创建方式
1 2 3 4 5 6 7 8 9 10 11 12 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 43 44 45 46 47 48 49 func stringify (v any) string { switch s := v.(type ) { case stringer: return s.String() case string : return s } return "<not Stringer>" } 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) } } }
源码 根ctx 根ctx有两个,background
和todo
,都是一个指向emptyCtx
的指针
1 2 3 4 5 6 type emptyCtx int var ( background = new(emptyCtx) todo = new(emptyCtx) )
而emptyCtx
实现Context
接口的方法都是返回nil
,无法取消,没有存储键值对,也不会返回错误。
cancelCtx
创建过程 1 2 3 4 5 6 7 8 9 10 11 12 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} }
1 2 3 4 5 6 7 8 type cancelCtx struct { Context mu sync.Mutex done atomic.Value children map [canceler]struct {} err error }
创建过程,其实是将父ctx
,放到ctx
结构体中,其中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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 type canceler interface { cancel(removeFromParent bool , err error ) Done() <-chan struct {} } 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(): } }() } } 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 } 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 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) } c.children = nil c.mu.Unlock() if removeFromParent { removeChild(c.Context, c) } } var closedchan = make (chan struct {})func init () { close (closedchan) }
取消过程 取消是通过一个Done channel
实现,要取消这个context
,则需要让所有c.Done()
停止阻塞,要么是close
这个channel
,要么是用一个closed
的channel
替换
源码
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 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 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) } c.children = nil c.mu.Unlock() if removeFromParent { removeChild(c.Context, c) } } func removeChild (parent Context, child canceler) { p, ok := parentCancelCtx(parent) if !ok { return } p.mu.Lock() if p.children != nil { delete (p.children, child) } p.mu.Unlock() }
默认从父ctx
中清除都是false
,为什么不是true
?
true
的情况指的是,调用WithCancel()
生成的cancel func
时
1 2 3 4 5 6 7 8 9 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) } }
直白点说,就是当本ctx cancel时,除了清理子ctx,还需要与父ctx断绝关系。
timerCtx
timerCtx
可以通过WithDeadline
或WithTimeout
创建出来
创建过程 WithTimeout
其实是用WithDeadline
创建出来的,直接看WithDeadline
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 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) { 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) } } func (c *timerCtx) Deadline() (deadline time.Time, ok bool ) { return c.deadline, true } func newCancelCtx (parent Context) cancelCtx { return cancelCtx{Context: parent} }
取消过程 指手动cancel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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() }
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 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} }
相比较而言,valueCtx
就很简单,存储键值即可。
但是可以看到,结构体中存放的是单一的键值,而不是map slice
之类的多指
获取值 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) 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) } } } func (c *cancelCtx) Value(key any) any { if key == &cancelCtxKey { return c } return value(c.Context, key) }
和链表有点像,只是它的方向相反:Context 指向它的父节点,链表则指向下一个节点。通过 WithValue 函数,可以创建层层的 valueCtx,存储 goroutine 间可以共享的变量。
它会顺着链路一直往上找,比较当前节点的 key 是否是要找的 key,如果是,则直接返回 value。否则,一直顺着 context 往前,最终找到根节点(一般是 emptyCtx),直接返回一个 nil。所以用 Value 方法的时候要判断结果是否为 nil。
因为查找方向是往上走的,所以,父节点没法获取子节点存储的值,子节点却可以获取父节点的值。
如何使用context context 会在函数传递间传递。只需要在适当的时间调用 cancel 函数向 goroutines 发出取消信号或者调用 Value 函数取出 context 中的值。
在官方博客里,对于使用 context 提出了几点建议:
不要将 Context 塞到结构体里。直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx。
不要向函数传入一个 nil 的 context,如果你实在不知道传什么,标准库给你准备好了一个 context:todo。
不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。
同一个 context 可能会被传递到多个 goroutine,别担心,context 是并发安全的。
有争议的其实是WithValue
的用法
适合于请求绑定,而不是于数据绑定。例如token,reqest_id
防止滥用