context 实例是不可变的,每一个都是新创建的。
context 包主要做两件事:安全传递数据和控制链路。
context 包的核心 API 有四个:
Context 接口核心 API 有四个:
context 包父子关系:
控制是从上而下的,查找是从下至上的。
context.WithValue 用于安全传递数据
安全传递数据,是指在请求执行上下文中线程安全地传递数据。
因为 Go 本身没有 thread-local 机制,所以大部分类似的功能都是借助于 context 来实现的。
type valueCtx struct {
Context
key, val any
}
在使用 ValueCtx 时需要注意一点:
示例:
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 用于控制链路。三者用法打通小异:
而后便是监听 Done() 返回的 channel,不管 是主动调用 cancel() 还是超时,都能从这个 channel 里面取出来数据。后面可以用 Err() 方法来判断究竟是哪种情况。
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 方法做了两件事:
timerCtx 也是装饰器模式:在已有 cancelCtx的基础上增加了超时的功能。
实现要点:
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:一般这种用 法我们会认为是定时任务,而不是超时控制。
这种超时控制有两个弊端:
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()
}
Golang中Context的使用场景