Golang - Context学习笔记

context 实例是不可变的,每一个都是新创建的

context 包主要做两件事:安全传递数据和控制链路。

context 包的核心 API 有四个:

  • context.WithValue:设置键值对,并且放回一个新的 context 实例
  • context.WithCancel
  • context.WithDeadline
  • context.WithTimeout:三者都返回一个可取消的 context 实例,和取消函数

Context 接口核心 API 有四个:

  • Deadline :返回过期时间,如果 ok 为 false,说明没有 (不常用)
  • Done:返回一个 channel,一般用于监听 Context 实例的信号,比如说过期,或者正常关闭。(常用
  • Err: 返回一个错误用于表达 Context 发生了什么。Canceled => 正常关闭,DeadlineExceeded => 过期超时。比较常用
  • context.Value:取值。非常常用

context 包父子关系

  • 当父亲取消或超时时,所有派生的子context 都被取消或者超时
  • 当找 key 的时候,子 context 先看自己有没有,没有则去祖先里找。

控制是从上而下的,查找是从下至上的。

安全传递数据

context.WithValue 用于安全传递数据

安全传递数据,是指在请求执行上下文中线程安全地传递数据。

因为 Go 本身没有 thread-local 机制,所以大部分类似的功能都是借助于 context 来实现的。

type valueCtx struct {
	Context
	key, val any
}

在使用 ValueCtx 时需要注意一点:

  • 这里的 key 不推荐设置为普通的 string 或者 int 类型,为了防止不同的中间件对这个key的覆盖。最好的情况是每个中间件使用一个自定义的key类型。(在实际使用中,也经常使用 string 作为 key,这里自己注意就好)

示例:

ctx := context.TODO()
ctx = context.WithValue(ctx, "key1", "0001")
ctx = context.WithValue(ctx, "key2", "0002")
ctx = context.WithValue(ctx, "key3", "0003")
ctx = context.WithValue(ctx, "key4", "0004")

fmt.Println(ctx.Value("key1"))

控制

context.WithCancel,context.WithDeadline,context.WithTimeout 用于控制链路。三者用法打通小异:

  • 没有设置过期时间,但是又需要再必要的时候取消,使用 WithCancel
  • 在固定时间点过期,使用 WithDeadline
  • 在一段时间后过期,使用 WithTimeout

而后便是监听 Done() 返回的 channel,不管 是主动调用 cancel() 还是超时,都能从这个 channel 里面取出来数据。后面可以用 Err() 方法来判断究竟是哪种情况。

cancelCtx 实现

cancelCtx 也是典型的装饰器模式:在已有Context 的基础上,加上取消的功能。

type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
	children map[canceler]struct{} // set to nil by the first cancel call
	err      error                 // set to non-nil by the first cancel call
}

核心实现:

  • Done 方法是通过类似于 double-check 的机制写的。这种原子操作和锁结合的用法比较罕见。(思考:能不能换成读写锁?)

    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{})
    }
    
  • children: 利用 children 来维护了所有的衍生节点

    children:核心是儿子把自己加进去父亲的 children 字段里面。

    但是因为 Context 里面存在非常多的层级, 所以父亲不一定是 cancelCtx,因此本质上 是找最近属于 cancelCtx 类型的祖先,然后 儿子把自己加进去。
    cancel 就是遍历 children,挨个调用 cancel。然后儿子调用孙子的 cancel,子子孙孙无穷匮也。

  • cancel 方法做了两件事:

    • 遍历所有的 children
    • 关闭 done 这个 channel:这个符合谁创建谁关闭的原子

timerCtx 实现

timerCtx 也是装饰器模式:在已有 cancelCtx的基础上增加了超时的功能。

实现要点:

  • WithTimeout 和 WithDeadline 本质一样
  • WithDeadline 里面,在创建 timerCtx 的时候利用 time.AfterFunc 来实现超时

context 最经典的用法是利用 context 来控制超时。控制超时,相当于我们同时监听两个 channel,一个是正常业务结束的 chnnel, Done返回的

func TestTimeoutExample(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	bsChan := make(chan struct{})
	go func() {
		slowBusiness()
		bsChan <- struct{}{}
	}()
	select {
	case <-ctx.Done():
		fmt.Println("timeout")
	case <-bsChan:
		fmt.Println("business end")
	}
}

func slowBusiness() {
	time.Sleep(2 * time.Second)
}

另外一种超时控制是采用 time.AfterFunc:一般这种用 法我们会认为是定时任务,而不是超时控制。
这种超时控制有两个弊端:

  • 如果不主动取消,那么 AfterFunc 是必然会执行的
  • 如果主动取消,那么在业务正常结束到主动取消之间,有一个短时间的时间差
func TestTimeoutTimeAfter(t *testing.T) {
	bsChan := make(chan struct{})
	go func() {
		slowBusiness()
		bsChan <- struct{}{}
	}()

	timer := time.AfterFunc(time.Second, func() {
		fmt.Println("timeout")
	})
	<-bsChan
	fmt.Println("business end")
	timer.Stop()
}

context 包使用注意事项

  • 一般函数使用 Context 的时候,会把这个参数放在第一个参数的位置。
  • 从来不把 nil 当做 Context 类型的参数值,可以使用 context.Background() 创建一个空的上下文对象,也不要使用 nil。
  • Context 只用来临时做函数之间的上下文透传,不能持久化 Context 或者把 Context 长久保存。把 Context 持久化到数据库、本地文件或者全局变量、缓存中都是错误的用法。
  • key 的类型不应该是字符串类型或者其它内建类型,否则容易在包之间使用 Context 时候产生冲突。使用 WithValue 时,key 的类型应该是自己定义的类型。
  • 常常使用 struct{} 作为底层类型定义 key 的类型。对于 exported key 的静态类型,常常是接口或者指针。这样可以尽量减少内存分配。
  • 所有公共方法,除非是 utils, helper 之类的方法,否则都加上 context 参数
  • 不要作为结构体字段,除非你的结构体本身也是表达一个上下文的概念。

Context 应用示例

Golang中Context的使用场景

你可能感兴趣的:(Go,golang,学习)