概览

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接口

img.png

 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 来实现超时