概览
context 包详解及一些实战。context一般用来做元数据到传递,控制协程取消、超时等。
我做的项目中实际用途(后文会贴代码):
-
用来做传递元数据,如auth中间件中将用户id等公共信息放到context中 ,让控制器快速获得到用户id等,而不是每次去查库,或作逻辑操作拿到用户ID。
-
每一次请求中,让db使用同一个connection,而不是切换连接,可以减少切换,更重要到是,使用同一个连接,来完成事务操作,不然切换连接其实不是同一个事务。
context 包的核心 API 有四个
- context.WithValue:设置键值对,并且返 回一个新的 context 实例
- context.WithCancel
- context.WithDeadline
- context.WithTimeout:三者都返回一个可 取消的 context 实例,和取消函数
注意:context 实例是不可变的,每一次都 是新创建的。
根context: emptyCtx 及 Context 接口
context是个接口类型
1type Context interface {
2 Deadline() (deadline time.Time, ok bool)
3 Done() <-chan struct{}
4 Err() error
5 Value(key any) any
6}
context.Background(), context.TODO 都是很常见的,都是内部的emptyCtx类型,它实现了context接口。 其实他们经常被作为参数 传入到 以下几个 方法到第一个参数中。
context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)
context.WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
context.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
1var (
2 background = new(emptyCtx)
3 todo = new(emptyCtx)
4)
5func Background() Context {
6 return background
7}
8func TODO() Context {
9 return todo
10}
11type emptyCtx int
12func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
13 return
14}
15func (*emptyCtx) Done() <-chan struct{} {
16 return nil
17}
18func (*emptyCtx) Err() error {
19 return nil
20}
21func (*emptyCtx) Value(key any) any {
22 return nil
23}
接口方法 Deadline()
Deadline() (deadline time.Time, ok bool)
Deadline :返回过期时间,如果 ok 为 false,没有设置过期时间。(不常用)
可以看 timerCtx 类型 Deadline的方法,它是设置过期时间的,所有ok返回的true 。
1type timerCtx struct {
2 cancelCtx
3 timer *time.Timer // Under cancelCtx.mu.
4
5 deadline time.Time
6}
7func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
8 return c.deadline, true
9}
另外说明 cancelCtx 它其他方法都是有 ,唯独没Deadline方法 , cancelCtx成员有Context接口,那么自然自己也是Context接口
1package main
2
3import "fmt"
4
5type I1 interface {
6 Done()
7 To()
8}
9
10type Cancel struct {
11 I1
12}
13
14func (c *Cancel) To() {
15 fmt.Println("to")
16}
17func (*Cancel) String() string {
18 return "c"
19}
20func main() {
21 var c I1 = &Cancel{}
22 c.To()
23 fmt.Println(c.Done)
24}
接口方法 Done()
Done() <-chan struct{}
返回一个只读的channel,一般用于监听 Context 实例 的信号,比如说过期,或者正常关闭。(常用)
接口方法 Err()
Err() error
返回一个错误用于表达 Context 发生了什么。
Done方法返回的context没关闭,则返回nil。
1// 发生了取消
2// Canceled is the error returned by Context.Err when the context is canceled.
3var Canceled = errors.New("context canceled")
4
5// 发生了过期、超时
6// DeadlineExceeded is the error returned by Context.Err when the context's
7// deadline passes.
8var DeadlineExceeded error = deadlineExceededError{}
接口方法Value(key any) any
其实就是看本context 有没有值,没有就往parent context找,没找到就是到最顶级到emptyctx的值就是nil
context的父子关系
特点:context 的实例之间存在父子关系:
- 当父亲取消或者超时,所有派生的子 context 都被取消或者超时
- 当找 key 的时候,子 context 先看自己 有没有,没有则去祖先里面找 控制是从上至下的,查找是从下至上的
父context无法访问子context内容。
在逼不得已的时候我们可以 在父 context 里面放一个 map,后续都是 修改这个 map
控制取消、超时
context 包提供了三个控制方法, WithCancel、WithDeadline 和 WithTimeout。三者用法大同小异:
- 没有过期时间,但是又需要在必要的时候取 消,使用 WithCancel
- 在固定时间点过期,使用 WithDeadline
- 在一段时间后过期,使用 WithTimeout
这3个方法返回的子context, 监听Done() 返回的 channel,不管 是主动调用 cancel() 还是超时,都能从这个 channel 里面取出来数据。 可以用 Err() 方法来判断究竟是哪种情况。
valueCtx 实现
valueCtx 用于存储 key-value 数据,特点:
- 典型的装饰器模式:在已有 Context 的基础上附加一个 存储 key-value 的功能
- 只能存储一个 key, val:为什么不用 map?
- map 要求 key 是 comparable 的,而我们可能用不 是 comparable 的 key
- context 包的设计理念就是将 Context 设计成不可变
1// A valueCtx carries a key-value pair. It implements Value for that key and
2// delegates all other calls to the embedded Context.
3type valueCtx struct {
4 Context
5 key, val any
6}
7
8func (c *valueCtx) Value(key any) any {
9 if c.key == key {
10 return c.val
11 }
12 // 递归往parent context 找,找到就停下了, 没找到会一直找到最顶级的emptyCtx,那么返回的就是nil。
13 return value(c.Context, key)
14}
cancelCtx 实现
1// A cancelCtx can be canceled. When canceled, it also cancels any children
2// that implement canceler.
3type cancelCtx struct {
4 Context
5
6 mu sync.Mutex // protects following fields
7 done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
8 children map[canceler]struct{} // set to nil by the first cancel call
9 err error // set to non-nil by the first cancel call
10}
cancelCtx 也是典型的装饰器模式:在已有 Context 的基础上,加上取消的功能。
Done 方法是通过类似于 double-check 的 机制写的,先尝试原子拿,没拿到,则加互斥锁, 再去看一遍,如没拿到才会去store 一个channel。
1func (c *cancelCtx) Done() <-chan struct{} {
2 d := c.done.Load()
3 if d != nil {
4 return d.(chan struct{})
5 }
6 c.mu.Lock()
7 defer c.mu.Unlock()
8 d = c.done.Load()
9 if d == nil {
10 d = make(chan struct{})
11 c.done.Store(d)
12 }
13 return d.(chan struct{})
14}
children:核心是子context把自己加进去父亲的 children 字段里面
因为Context里面存在非常多的层级,所以父亲不一定是cancelCtx,因此本质上是找最近属于cancelCtx类型的祖先,然后儿子把自己加进去。
1// propagateCancel arranges for child to be canceled when parent is.
2func propagateCancel(parent Context, child canceler) {
3 done := parent.Done()
4 if done == nil {
5 return // parent is never canceled
6 }
7 //判断一遍是否 parent是否已经已经取消了
8 select {
9 case <-done:
10 // parent is already canceled
11 child.cancel(false, parent.Err())
12 return
13 default:
14 }
15 // parentCancelCtx这个方法就是找到 最近的cancelCtx的祖先,找到了就在它到children中去放入child context
16 if p, ok := parentCancelCtx(parent); ok {
17 p.mu.Lock()
18 if p.err != nil {
19 // parent has already been canceled
20 child.cancel(false, p.err)
21 } else {
22 if p.children == nil {
23 p.children = make(map[canceler]struct{})
24 }
25 //将子context 放到 最近的 cancelCtx类型祖先的 children中
26 p.children[child] = struct{}{}
27 }
28 p.mu.Unlock()
29 } else {
30 //这个计数器只为测试使用
31 atomic.AddInt32(&goroutines, +1)
32 go func() {
33 select {
34 case <-parent.Done():
35 child.cancel(false, parent.Err())
36 case <-child.Done():
37 }
38 }()
39 }
40}
cancel() 方法解读
手动cancel() 就是关闭它所监听的done chanel。 并把它下面所有子context也尝试去关闭一遍。
1func (c *cancelCtx) cancel(removeFromParent bool, err error) {
2 // Canceled = errors.New("context canceled") 肯定是传这个err
3 if err == nil {
4 panic("context: internal error: missing cancel error")
5 }
6 c.mu.Lock()
7 if c.err != nil {
8 c.mu.Unlock()
9 return // already canceled
10 }
11 c.err = err
12 d, _ := c.done.Load().(chan struct{})
13 if d == nil {
14 //没有done chan 就放个 的已关闭的chan进去
15 c.done.Store(closedchan)
16 } else {
17 close(d)
18 }
19 for child := range c.children {
20 // NOTE: acquiring the child's lock while holding parent's lock.
21 child.cancel(false, err)
22 }
23 c.children = nil
24 c.mu.Unlock()
25
26 if removeFromParent {
27 removeChild(c.Context, c)
28 }
29}
timerCtx 实现
timerCtx 也是装饰器模式:在已有 cancelCtx 的基础上增加了超时的功能。
WithTimeout 和 WithDeadline 本质一样
WithDeadline 里面,在创建 timerCtx 的时候利用 time.AfterFunc 来实现超时