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 参数
- 不要作为结构体字段,除非你的结构体本身也是表达一个上下文的概念。