前言
在go开发中经常使用到context,使用的时候感觉和其他语言(py的flask)上下文有点类似,所以激起我的好奇心,看看go的context都干了啥。看了下源码不多,所以花了点时间,把源码稍微看了看。
什么是Context?
Context 也叫作“上下文”,一般理解为程序单元的一个运行状态、环境、快照等信息。其中上下是指存在上下层的传递,上会把内容传递给下,程序单元则指的是 Goroutine。
Context的作用
context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等(后面会放出我使用案例)。为了方便以下ctx等价于context
简单例子
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
ticker := time.NewTicker(time.Second * 1)
defer func() {
cancel()
ticker.Stop()
}()
for {
isBreak := false
select {
case <-ticker.C:
fmt.Println("木子林")
case <-ctx.Done():
isBreak = true
fmt.Println(ctx.Err())
}
if isBreak{
break
}
}
}
源码梳理
Context接口
Context是Go 语言 context
包对外暴露的接口,该接口定义了四个需要实现的方法:
-
Deadline() : 返回 context 的截止时间,通过此时间,函数就可以决定是否进行接下来的操作,如果时间太短,就可以不往下做了,否则浪费系统资源。当然,也可以用这个 deadline 来设置一个 I/O 操作的超时时间。
-
Done(): 返回一个 channel,可以表示 context 被取消的信号 。(PS:当这个 channel 被关闭时,说明 context 被取消了。注意,这是一个只读的channel。 我们又知道,读一个关闭的 channel 会读出相应类型的零值。并且源码里没有地方会向这个 channel 里面塞入值。换句话说,这是一个
receive-only
的 channel。因此在子协程里读这个 channel,除非被关闭,否则读不出来任何东西。也正是利用了这一点,子协程从 channel 里读出了值(ni l)后,就可以做一些收尾工作,尽快退出。) -
Err(): 返回一个错误,表示 channel 被关闭的原因,比如说:尚未关闭Done,Err将返回nil;关闭“完成”,Err将返回一个非零错误。
-
Value(key interface{}):Value返回键对应的值,如果没有值与键关联,则返回nil。
ps: 这四个方法是context最主要的方法,后面所有的代码都基于这四个方法展开
对外方法
1- background
上下文中最顶层的默认值,通常由主函数、初始化和测试使用,并作为传入请求的顶级上下文,作为所有 context 的根节点
2- todo
不清楚要使用哪个上下文,或者它还不可用(因为周围的函数还没有扩展到接受上下文参数)。例如,调用一个需要传递 context 参数的函数,你手头并没有其他 context 可以传递,这时就可以传递 todo,来占个位子,最终要换成其他 context。
内部实现起来比较简单,直接new(emptyCtx)就行了,对外使用Background(), TODO()对外使用(大写外部可调用:context.Background(), context.TODO())
emptyCtx: 实际上是一个空的 context,永远不会被 cancel,没有存储值,也没有 deadline。emptyCtx不是 struct{},因为emptyCtx类型的变量必须有不同的地址
ps: 从String的方法中可以看出,只返回backgrounders,todo对应信息,其他的都返回未识别
3-WithCancel
WithCancel:方法能够从 Context 中创建出一个新的子上下文,同时还会返回用于取消该上下文的函数,也就是 CancelFunc
withCancel 第一步就是new 一个新的 ctx, cancelCtx是一个可以取消的 Context,实现了 canceler 接口。它直接将接口 Context 作为它的一个匿名字段,这样,它就可以被看成一个 Context。
cancelCtx
Value方法和Err方法没什么好说的,其中Value中的&cancelCtxKey表示cancelCtx为其自身返回的key。 我们下面主要说下Done和cancel方法:
done源码
done 只有调用了 Done() 方法的时候才会被创建,done是懒式加载。函数返回的是一个只读的 channel,而且没有地方向这个 channel 里面写数据。所以,直接调用读这个 channel,协程会被 block 住。一般通过搭配 select 来使用。一旦关闭,就会立即读出零值。
cancel源码
1: 如果已经取消了,则直接返回
2:关闭c t x,需要将done 设为 已关闭channel或关闭 Done channel
3:递归所有的子级,进行取消
4: 如果removeFromParent为true则从父节点移除自己
propagateCancel:父对象找到可取消的子级
-
当 parent.Done() == nil,也就是 parent 不会触发取消事件时,当前函数直接返回;
-
当 child 的继承链上有 parent 是可以取消的上下文时,就会判断 parent是否已经触发了取消信号:
-
如果已经被取消,当前 child 就会立刻被取消;
-
如果没有被取消,当前 child 就会被加入 parent 的 children 列表中,等待 parent 释放取消信号;
-
-
如果没有可取消的parent,会开启一个新的 Goroutine,同时监听parent.Done() 和child.Done()两个管道。作用在于:
-
第一个 case 说明当父节点取消,则取消子节点。如果去掉这个 case,那么父节点取消的信号就不能传递到子节点
-
第二个 case 是说如果子节点自己取消了,那就退出这个 select,父节点的取消信号就不用管了。如果去掉这个 case,那么很可能父节点一直不取消,这个 goroutine 就泄漏了。当然,如果父节点取消了,就会重复让子节点取消。
-
parentCancelCtx: 返回父级的基础*cancelCtx
-
如果没有子级或者已经取消直接返回
-
parentCancelCtx返回父级的基础*cancelCtx,查询出来的与Done进行比对
4- WithDeadline
WithDeadline: 创建 timerCtx 上下文的过程中,判断了上下文的截止日期与当前日期,并通过time.AfterFunc 方法创建了定时器,当时间超过了截止日期之后就会调用 cancel 方法同步取消信号
- 如果已经过期则直接调用WithCancel
- 如果执行到这里过期了,直接取消
- 如果还没有存在,则挂载一个定时任务,定时去取消
timerCtx
timerCtx 基于 cancelCtx,只是多了一个 time.Timer 和一个 deadline。Timer 会在 deadline 到来时,自动取消 context
timerCtx 提供了主动取消,当调用主动取消则需要关闭定时。
5- WithTimeout
基于父 context,返回带超时时间的子 context 和取消函数
6- WithValue
WithValue返回和键关联的值为val的父项的副本 ,从父上下文中创建一个子上下文,传值的子上下文使用私有结构体 valueCtx
valueCtx
如果当前 valueCtx 中存储的键与 Value 方法中传入的不匹配,就会从父上下文中查找该键对应的值直到在某个父上下文中返回 nil 或者查找到对应的值。
官方博客里对context 使用提出了几点建议
-
不要将 Context 塞到结构体里。直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx。
-
不要向函数传入一个 nil 的 context,如果你实在不知道传什么,标准库给你准备好了一个 context:todo。
-
不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。
-
同一个 context 可能会被传递到多个 goroutine,别担心,context 是并发安全的。