作用
在goroutine调用树中传递通知或者数据。context.Context可在多个goroutine之间进行传递,相比于channel,它规定了接口,更加方便使用,规范性更强。
-
退出通知:通知可以传递给goroutine调用树上的每个goroutine,使得在多级goroutine调用链上取消动作更加容易,可以方便地取消调用链上的任何一个goroutine。
💡 需要注意的是,还是需要我们自己手动处理Done()的返回值来决定是否退出goroutine,context只是负责传递信息,但是停止goroutine的行为还是要自己处理。
-
传递数据:数据可以传递给goroutine调用树上的每个goroutine
context.Context
理解
- context.Context是一个接口类型,是go标准库中为用户提供的使用context的统一格式类型;
- 需要使用Context的时候,就使用context.Context接口类型,一般在函数的第一个参数中传入context.Context
4个接口方法
context.Context接口的全部方法体现了其作用:传递通知和传递数据,4个接口方法定义如下:
// 传递退出通知
// 当Context被取消,Done()函数返回的是一个已经关闭的channel,从而可以使得多个goroutine得到通知。无论是主动调用CancelFunc还是Context设定的定时时间到期,都会通知退出。
func Done() ←chan struct{}
// 传递数据
// 通过key-value的形式传递数据,一个Context只能传递一对key-value
func Value(any) any
// 获取Context退出的原因
func Err() error
// 获取Context是否设置了最后期限,如果设置了,则bool返回true,并且返回最后期限的时间;否则返回bool为false
func Deadline() (time.Time, bool)
4个具体的Context类型和API
- emptyCtx
func Background() Context
func TODO() Context
- cancelCtx
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
- timerCtx
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
- valueCtx
func WithValue(parent Context, key, val any) Context
Context的层级关系
💡 一个Context可以作为其它Context的父Context,只需要在使用API创建Context的时候指定。
示例
// parent context
// ctx_parent -> ctx_level1 -> ctx_level2
ctx_parent, cancel := context.WithCancel(context.Background())
// level1 context
// 6s后过期
ctx_level1, _ := context.WithTimeout(ctx_parent, time.Second * 6)
// level2 context
// level1过期的话,level2也会跟着过期
ctx_level2, _ := context.WithCancel(ctx_level1)
var wg sync.WaitGroup
f := func(ctx context.Context, level int, interval int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("level-%d received done\n", level)
return
default:
fmt.Printf("level-%d running\n", level)
time.Sleep(time.Duration(interval) * time.Second)
}
}
}
wg.Add(2)
go f(ctx_parent, 0, 1)
go f(ctx_level1, 1, 2)
go f(ctx_level2, 2, 1)
wg.Wait()
wg.Add(1)
cancel()
wg.Wait()
工作方式
-
通知退出
父Context
的退出通知会向下传递到子Context
但是
子Context
的退出通知则不会向上传递到父Context
-
值传递
子Context
可以找到父Context
中的key-value
但是
父Context
找不到子Context
中的key-value
,示例如下root := context.Background() ctx_parent := context.WithValue(root, keyType("parent"), "parent-value") ctx_child := context.WithValue(ctx_parent, keyType("child"), "child-value") var wg sync.WaitGroup type keyType string // WithValue需要使用自定义的key类型,防止和使用context的库造成冲突 f := func(ctx context.Context, name string, keyname keyType) { defer wg.Done() fmt.Printf("[goroutine %s] ctx[%v]=%v\n", name, keyname, ctx.Value(keyname)) } wg.Add(2) go f(ctx_parent, "goroutine-1", keyType("child")) // 拿不到 key为child的值 go f(ctx_child, "goroutine-2", keyType("parent")) // 可以拿到key为parent的值 wg.Wait()
其它细节和注意事项
- 不要将
context.Context
作为一个struct的成员,而是应该通过函数传参的方式传递,通常Context都放在第一个参数,名字通常命名为ctx - 不要传递值为
nil
的Context,应该要用context.TODO()
代替 - context的Value不应该用来传递一些和业务相关的参数(not for passing optional parameters to functions.)
- Context是并发安全的(Contexts are safe for simultaneous use by multiple goroutines)
WithDeadline(parent context.Context, d time.Time)
,如果parent也设置有deadline,并且parent的deadline已经比d要早了,那么WithDeadline返回的Context的deadline时间以parent为准CancelFunc
类型,这个函数类型是With系列函数的第二个返回值(除WithValue),可以直接调用这个函数,用来广播退出通知。
在不同的goroutine中同时调用CancelFunc
是并发安全的,如果同时调用的话,首次调用后的后续调用不影响。(A CancelFunc may be called by multiple goroutines simultaneously. After the first call, subsequent calls to a CancelFunc do nothing.)