context是什么
context翻译成中文就是上下文,在软件开发环境中,是指接口之间或函数调用之间,除了传递业务参数之外的额外信息,像在微服务环境中,传递追踪信息traceID, 请求接收和返回时间,以及登录操作用户的身份等等。本文说的context是指golang标准库中的context包。Go标准库中的context包,提供了goroutine之间的传递信息的机制,信号同步,除此之外还有超时(timeout)和取消(cancel)机制。概括起来,Context可以控制子goroutine的运行,超时控制的方法调用,可以取消的方法调用。
为什么需要context
根据前面的Context的介绍,Context可以控制goroutine的运行,超时、取消方法的调用。对于这些功能,有没有别的实现方法。当然是有的,控制goroutine的运行,可以通过select+channel的机制实现,超时控制也可以通过ticker实现,取消方法调用也可以向channel中发送信号,通知方法退出。既然Context能实现的功能,也有别的方式能够实现,那为啥还要Context呢?在一些复杂的场景中,通过channel等方式控制非常繁琐,而采用Context可以很方便的实现上述功能。场景1:主协程启动了m个子协程,分别编号为g1,g2,...gm。对于g1协程,它又启动了n个子协程,分别编号为g11,g12,...g1n。现在希望主协程取消的时候或g1取消的时候,g1下面的所有子协程也取消执行,采用channel的方法,需要申请2个channel, 一个是主协程退出通知的channel,另一个是g1退出时的channel。g1的所有子协程需要同时select这2个channel。现在是2层,用channel还能接受,如果层级非常深,那监控起来需要很多的channel, 操作非常繁琐。采用Context可以简单的达到上述效果,不用申请一堆channel。场景2: 在微服务中,任务A运行依赖于下游的任务B, 考虑到任务B可能存在服务不可用,所以通常在任务A中会加入超时返回逻辑,需要开一个定时器,同时任务A也受控于父协程,当父协程退出时,希望任务A也退出,那么在任务A中也要监控父协程通过channle发送的取消信息,那有没有一种方式将这两种情况都搞定,不用即申请定时器又申请channel,因为他们的目的都是取消任务A的运行嘛,Context就能搞定这种场景。
context源码解析
下面的源码解析的是go的最新版本1.14.2
结构图
context定义了2大接口,Context和canceler
, 结构体类型*emptyCtx,*valueCtx实现了Context接口,*cancelCtx同时实现了Context接口和cancelr接口,*timerCtx内嵌了cancelCtx,它也间接实现了Context和canceler接口。类型结构如下
函数、结构体和变量说明
名称 | 类型 | 可否导出 | 说明 |
---|---|---|---|
Context | 接口 | 可以 | Context最基本接口,定义了4个方法 |
canceler | 接口 | 不可以 | Context取消接口,定义了2个方法 |
emptyCtx | 结构体 | 不可以 | 实现了Context接口,默认都是空实现,emptyCtx是int类型别名 |
cancelCtx | 结构体 | 不可以 | 可以被取消 |
valueCtx | 结构体 | 不可以 | 可以存储key-value信息 |
timerCtx | 结构体 | 不可以 | 可被取消,也可超时取消 |
CancelFunc | 函数 | 可以 | 取消函数签名 |
Background | 函数 | 可以 | 返回一个空的Context,常用来作为根Context |
Todo | 函数 | 可以 | 返回一个空的 context,常用于初期写的时候,没有合适的context可用 |
WithCancel | 函数 | 可以 | 理解为产生可取消Context的构造函数 |
WithDeadline | 函数 | 可以 | 理解为产生可超时取消Context的构造函数 |
WithTimeout | 函数 | 可以 | 理解为产生可超时取消Context的构造函数 |
WithValue | 函数 | 可以 | 理解为产生key-value Context的构造函数 |
newCancelCtx | 函数 | 不可以 | 创建一个可取消的Context |
propagateCancel | 函数 | 不可以 | 向下传递 context 节点间的取消关系 |
parentCancelCtx | 函数 | 不可以 | 找到最先出现的一个可取消Context |
removeChild | 函数 | 不可以 | 将当前的canceler从父Context中的children map中移除 |
background | 变量 | 不可以 | 包级Context,默认的Context,常作为顶级Context |
todo | 变量 | 不可以 | 包级Context,默认的Context实现,也作为顶级Context,与background同类型 |
closedchan | 变量 | 不可以 | channel struct{}类型,用于信息通知 |
Canceled | 变量 | 可以 | 取消error |
DeadlineExceeded | 变量 | 可以 | 超时error |
cancelCtxKey | 变量 | 不可以 | int类型别名,做标记用的 |
Context接口
Context具体实现包括4个方法,分别是Deadline、Done、Err和Value,如下所示,每个方法都加了注解说明。
// Context接口,下面定义的四个方法都是幂等的
type Context interface {
// 返回这个Context被取消的截止时间,如果没有设置截止时间,ok的值返回的是false,
// 后续每次调用对象的Deadline方法是,返回的结果都和第一次相同,即具有幂等性
Deadline() (deadline time.Time, ok bool)
// 返回一个channel对象,在Context被取消时,此channel会被close。如果没有被
// 取消,可能返回nil。每次调用Done总是会返回相同的结果,当channel被close的时候,
// 可以通过ctx.Err获取错误信息
Done() <-chan struct{}
// 返回一个error对象,当channel没有被close的时候,Err方法返回nil,如果channel被
// close, Err方法会返回channel被close的原因,可能是被cancel,deadline或timeout取消
Err() error
// 返回此cxt中指定key对应的value
Value(key interface{}) interface{}
}
canceler接口
canceler接口定义如下所示,如果一个Context类型实现了下面定义的2个方法,该Context就是一个可取消的Context。Context包中结构体指针*cancelCtx和*timerCtx
实现了canceler接口。
-
为啥不将这里的canceler接口与Context接口合并呢?况且他们定义的方法中都有Done方法,可以解释得通的说法是,源码作者认为cancel方法并不是Context必须的,根据最小接口设计原则,将两者分开。像emptyCtx和valueCtx不是可取消的&