Go中Context使用源码解析
作者:程序员wall
前言
本篇内容的主题是Go中Context,想必已学习Go语言的大家在熟悉不过了。工作中我们也常会用到,但有时很少去注意它。
本打算将相关知识点归总一下,发现其源码不多,就打算对其源码进行分析一下。
context
包是在go1.17是引入到标准库中,且标准库中大部分接口都将context.Context
作为第一个参数。
context
中文译为“上下文”,实际代表的是goroutine的上下文。且多用于超时控制和多个goroutine间的数据传递。
本篇文章将带领大家深入了解其内部的工作原理。
1、Context定义
Context 接口定义如下
type Context interface { // Deadline returns the time when this Context will be canceled, if any. Deadline() (deadline time.Time, ok bool) // Done returns a channel that is closed when this Context is canceled // or times out. Done() <-chan struct{} // Err indicates why this context was canceled, after the Done channel // is closed. Err() error // Value returns the value associated with key or nil if none. Value(key any) any }
Deadline()
: 返回的第一个值是 截止时间,到了这个时间点,Context 会自动触发 Cancel 动作。返回的第二个值是 一个布尔值,true 表示设置了截止时间,false 表示没有设置截止时间,如果没有设置截止时间,就要手动调用 cancel 函数取消 Context。
Done()
: 返回一个只读的通道(只有在被cancel后才会返回),类型为 struct{}
。当这个通道可读时,意味着parent context已经发起了取消请求,根据这个信号,开发者就可以做一些清理动作,退出goroutine。这里就简称信号通道吧!
Err()
:返回Context 被取消的原因
Value
: 从Context中获取与Key关联的值,如果没有就返回nil
2、Context的派生
2.1、创建Context对象
context
包提供了四种方法来创建context对象,具体方法如下:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {} func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {} func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {} func WithValue(parent Context, key, val any) Context {}
由以上方法可知:新的context对象都是基于父context对象衍生的.
WithCancel
:创建可以取消的ContextWithDeadline
: 创建带有截止时间的ContextWithTimeout
:创建带有超时时间的Context,底层调用的是WithDeadline
方法WithValue
:创建可以携带KV型数据的Context
简单的树状关系如下(实际可衍生很多中):
2.2、parent Context
context
包默认提供了两个根context 对象background
和todo
;看实现两者都是由emptyCtx创建的,两者的区别主要在语义上,
- context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生出来;
- context.TODO 应该仅在不确定应该使用哪种上下文时使用
var ( background = new(emptyCtx) todo = new(emptyCtx) ) // Background 创建background context func Background() Context { return background } // TODO 创建todo context func TODO() Context { return todo }
3、context 接口四种实现
具体结构如下,我们大致看下相关结构体中包含的字段,具体字段的含义及作用将在下面分析中会提及。
- emptyCtx
type emptyCtx int // 空context
- cancelCtx
type cancelCtx struct { Context // 父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 // cancel的原因 }
- timerCtx
type timerCtx struct { cancelCtx //父context timer *time.Timer // 定时器 deadline time.Time // 截止时间 }
- valueCtx
type valueCtx struct { Context // 父context key, val any // kv键值对 }
4、 emptyCtx 源码分析
emptyCtx
实现非常简单,具体代码如下,我们简单看看就可以了
// An emptyCtx is never canceled, has no values, and has no deadline. It is not // struct{}, since vars of this type must have distinct addresses. type emptyCtx int func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { return } func (*emptyCtx) Done() <-chan struct{} { return nil } func (*emptyCtx) Err() error { return nil } func (*emptyCtx) Value(key any) any { return nil } func (e *emptyCtx) String() string { switch e { case background: return "context.Background" case todo: return "context.TODO" } return "unknown empty Context" }
5、 cancelCtx 源码分析
cancelCtx
的实现相对复杂点,比如下面要介绍的timeCtx 底层也依赖它,所以弄懂cancelCtx
的工作原理就能很好的理解context
.
cancelCtx
不仅实现了Context
接口也实现了canceler
接口
5.1、对象创建withCancel()
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { if parent == nil { // 参数校验 panic("cannot create context from nil parent") } // cancelCtx 初始化 c := newCancelCtx(parent) propagateCancel(parent, &c) // cancelCtx 父子关系维护及传播取消信号 return &c, func() { c.cancel(true, Canceled) } // 返回cancelCtx对象及cannel方法 } // newCancelCtx returns an initialized cancelCtx. func newCancelCtx(parent Context) cancelCtx { return cancelCtx{Context: parent} }
用户调用WithCancel
方法,传入一个父 Context(这通常是一个 background
,作为根节点),返回新建的 context,并通过闭包的形式返回了一个 cancel 方法。如果想要取消context时需手动调用cancel方法。
5.1.1、newCancelCtx
cancelCtx对象初始化, 其结构如下:
type cancelCtx struct { // 父 context Context // parent context // 锁 并发场景下保护cancelCtx结构中字段属性的设置 mu sync.Mutex // protects following fields // done里存储的是信号通道,其创建方式采用的是懒加载的方式 done atomic.Value // of chan struct{}, created lazily, closed by first cancel call // 记录与父子cancelCtx对象, children map[canceler]struct{} // set to nil by the first cancel call // 记录ctx被取消的原因 err error // set to non-nil by the first cancel call }
5.1.2、propagateCancel
propagateCancel
// propagateCancel arranges for child to be canceled when parent is. func propagateCancel(parent Context, child canceler) { done := parent.Done() // 获取parent ctx的信号通道 done if done == nil { // nil 代表 parent ctx 不是canelctx 类型,不会被取消,直接返回 return // parent is never canceled } select { // parent ctx 是cancelCtx类型,判断其是否被取消 case <-done: // parent is already canceled child.cancel(false, parent.Err()) return default: } //parentCancelCtx往树的根节点方向找到最近的context是cancelCtx类型的 if p, ok := parentCancelCtx(parent); ok { // 查询到 p.mu.Lock() // 加锁 if p.err != nil { // 祖父 ctx 已经被取消了,则 子cancelCtx 也需要调用cancel 方法来取消 // parent has already been canceled child.cancel(false, p.err) } else { // 使用map结构来维护 将child加入到祖父context中 if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock()// 解锁 } else { // 开启协程监听 parent Ctx的取消信号 来通知child ctx 取消 atomic.AddInt32(&goroutines, +1) go func() { select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): } }() } }
// parentCancelCtx returns the underlying *cancelCtx for parent. // It does this by looking up parent.Value(&cancelCtxKey) to find // the innermost enclosing *cancelCtx and then checking whether // parent.Done() matches that *cancelCtx. (If not, the *cancelCtx // has been wrapped in a custom implementation providing a // different done channel, in which case we should not bypass it.) // parentCancelCtx往树的根节点方向找到最近的context是cancelCtx类型的 func parentCancelCtx(parent Context) (*cancelCtx, bool) { done := parent.Done() // closedchan 代表此时cancelCtx 已取消, nil 代表 ctx不是cancelCtx 类型的且不会被取消 if done == closedchan || done == nil { return nil, false } // 向上遍历查询canelCtx 类型的ctx p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) if !ok { // 没有 return nil, false } // 存在判断信号通道是不是相同 pdone, _ := p.done.Load().(chan struct{}) if pdone != done { return nil, false } return p, true }
5.2 canceler
cancelCtx
也实现了canceler
接口,实现可以 取消上下文的功能。
canceler
接口定义如下:
// A canceler is a context type that can be canceled directly. The // implementations are *cancelCtx and *timerCtx. type canceler interface { cancel(removeFromParent bool, err error) // 取消 Done() <-chan struct{} // 只读通道,简称取消信号通道 }
cancelCtx
接口实现如下:
整体逻辑不复杂,逻辑简化如下:
- 当前 cancelCtx 取消 且 与之其关联的子 cancelCtx 也取消
- 根据removeFromParent标识来判断是否将子 cancelCtx 移除
注意
由于信号通道的初始化采用的懒加载方式,所以有未初始化的情况;
已初始化的:调用close 函数关闭channel
未初始化的:用 closedchan
初始化,其closedchan
是已经关闭的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) } }
// removeChild removes a context from its parent. func removeChild(parent Context, child canceler) { p, ok := parentCancelCtx(parent) if !ok { return } p.mu.Lock() if p.children != nil { delete(p.children, child) } p.mu.Unlock() }
closedchan
可重用的关闭通道,该channel通道默认已关闭
// closedchan is a reusable closed channel. var closedchan = make(chan struct{}) func init() { close(closedchan) // 调用close 方法关闭 }
6、timerCtx 源码分析
cancelCtx
源码已经分析完毕,那timerCtx
理解起来就很容易。
关注点:timerCtx
是如何取消上下文的,以及取消上下文的方式
6.1、对象创建 WithDeadline和WithTimeout
WithTimeout
底层调用是WithDeadline 方法 ,截止时间是 now+timeout;
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) }
WithDeadline
整体逻辑并不复杂,从源码中可分析出timerCtx
取消上下文 采用两种方式 自动和手动;其中自动方式采用定时器去处理,到达触发时刻,自动调用cancel方法。
deadline
: 截止时间
timer *time.Timer
: 定时器
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if parent == nil { panic("cannot create context from nil parent") } if cur, ok := parent.Deadline(); ok && cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) } c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } propagateCancel(parent, c) dur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded) // deadline has already passed return c, func() { c.cancel(false, Canceled) } } c.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) } }
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to // implement Done and Err. It implements cancel by stopping its timer then // delegating to cancelCtx.cancel. type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time }
6.2 timerCtx的cancel
- 调用cancelCtx的cancel 方法
- 根据removeFromParent标识,为true 调用removeChild 方法 从它的父cancelCtx的children中移除
- 关闭定时器 ,防止内存泄漏(着重点)
func (c *timerCtx) cancel(removeFromParent bool, err error) { c.cancelCtx.cancel(false, err) if removeFromParent { // Remove this timerCtx from its parent cancelCtx's children. removeChild(c.cancelCtx.Context, c) } c.mu.Lock() if c.timer != nil { c.timer.Stop() c.timer = nil } c.mu.Unlock() }
7、valueCtx 源码分析
7.1、对象创建WithValue
valueCtx
结构体中有key
和val
两个字段,WithValue
方法也是将数据存放在该字段上
func WithValue(parent Context, key, val any) Context { if parent == nil { panic("cannot create context from nil parent") } if key == nil { panic("nil key") } if !reflectlite.TypeOf(key).Comparable() { panic("key is not comparable") } return &valueCtx{parent, key, val} } // A valueCtx carries a key-value pair. It implements Value for that key and // delegates all other calls to the embedded Context. type valueCtx struct { Context key, val any }
7.2、获取value值
func (c *valueCtx) Value(key any) any { if c.key == key { // 判断当前valuectx对象中的key是否匹配 return c.val } return value(c.Context, key) } // value() 向根部方向遍历,直到找到与key对应的值 func value(c Context, key any) any { for { switch ctx := c.(type) { case *valueCtx: if key == ctx.key { return ctx.val } c = ctx.Context case *cancelCtx: if key == &cancelCtxKey { // 获取cancelCtx对象 return c } c = ctx.Context case *timerCtx: if key == &cancelCtxKey { return &ctx.cancelCtx } c = ctx.Context case *emptyCtx: return nil default: return c.Value(key) } } }
总结:从Context
中获取对应的值需要通过遍历的方式来获取,这里告诫我们嵌套太多的context反而对性能会有影响
8、规范&注意事项
- 不要把context存在一个结构体当中,显式地传入函数。context变量需要作为第一个参数使用,一般命名为ctx;
- 即使方法允许,也不要传入一个nil的Context,如果你不确定你要用什么Context的时候传一个context.TODO
- 使用context的Value相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数
- 同样的Context可以用来传递到不同的goroutine中,Context在多个goroutine中是安全的
以上就是Go中Context使用源码解析的详细内容,更多关于Go Context源码解析的资料请关注脚本之家其它相关文章!