context也就是说的上下文。
context 包我们就用来做两件事:
- 安全传递数据 :是指在请求执行上下文中线 程安全地传递数据,依赖于 WithValue 方法
- 控制链路
使用场景比较丰富: 链路追踪的 trace id 、 AB测试的标记位 、 压力测试标记位 、分库分表中间件中传递 sharding hint 、ORM 中间件传递 SQL hint 、 Web 框架传递上下文等
打开context包源码,大致内容讲解如下:
context包定义了context类型,它跨越API边界和进程之间携带截止日期、取消信号和其他请求范围的值。
向服务器的传入请求应该创建一个Context,而向服务器发出的调用应该接受一个Context。它们之间的函数调用链必须传播上下文,可以选择用使用WithCancel、WithDeadline、WithTimeout或WithValue创建的派生上下文替换它。当一个上下文被取消时,从它派生的所有上下文也被取消。
WithCancel, WithDeadline和WithTimeout函数接受一个Context(父函数),并返回一个派生的Context(子函数)和一个CancelFunc。调用CancelFunc会取消子进程及其子进程,移除父进程对子进程的引用,并停止任何相关的计时器。未能调用CancelFunc会泄漏子进程及其子进程,直到父进程被取消或计时器触发。go vet工具检查CancelFuncs是否在所有控制流路径上使用。
使用上下文的程序应该遵循这些规则,以保持跨包的接口一致,并启用静态分析工具来检查上下文传播:
func DoSomething(ctx context.Context, arg Arg) error {
// ... use ctx ...
}
context 包的核心 API 有四个:
context.WithValue
:设置键值对,并且 返回一个新的 context 实例context.WithCancel
context.WithDeadline
context.WithTimeout
:三者都返回一个 可取消的 context 实例,和取消函数注意:context 实例是不可变的,每一次都是 新创建的。
context.WithValue 用于安全传递数据
另外三个,WithCancel
,WithDeadline
,WithTimeout
用于控制链路
Context 接口核心 API 有四个:
context实例之间存在父子关系。
valueCtx定义如下:
type valueCtx struct {
Context
key, val any
}
valueCtx 用于存储 key-value 数据,特点:
在查找值的时候,先从自己查找,不行再找父亲的
context父亲可以控制儿子,但是儿子控制不了父亲
context 包提供了三个控制方法, WithCancel
、WithDeadline
和 WithTimeout
。
三者用法大同小异:
WithCancel
没有过期时间,但是又需要在必要的时候取 消,使用 WithCancelWithDeadline
在固定时间点过期,使用 WithDeadlineWithTimeout
在一段时间后过期,使用 WithTimeoutcontext最经典的用法:控制超时,相当于我们同时监听两个 channel,一个是正常业务结束的 channel,一个Done() 返回的。
超时控制至少两个分支:
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
// goroutines counts the number of goroutines ever created; for testing.
var goroutines int32
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
if done == nil {
return // parent is never canceled
}
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
其定义如下
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
}
cancelCtx 也是典型的装饰器模式:在已有 Context 的基础上,加上取消的功能。
核心实现:
children:核心是儿子把自己加进去父亲的 children 字段里面。 但是因为 Context 里面存在非常多的层级, 所以父亲不一定是 cancelCtx,因此本质上是找最近属于 cancelCtx 类型的祖先,然后儿 子把自己加进去。 cancel 就是遍历 children,挨个调用 cancel。然后儿子调用孙子的 cancel。
做两件事:
• 遍历所有的 children
• 关闭 done 这个 channel:这个符合谁创 建谁关闭的原则
源码如下
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
是装饰器模式:在已有 cancelCtx 的 基础上增加了超时的功能。
实现要点:
• 一般只用做方法参数,而且是作为第一个参数;
• 所有公共方法,除非是 util,helper 之类的方法,否则都加上 context 参数;
• 不要用作结构体字段,除非你的结构体本身也是表达一个上下文的概念。