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不是可取消的,所以他们只要实现Context接口即可。cancelCtx和timerCtx是可取消的Context,他们要实现2个接口中的所有方法。
-
WithCancel提供了创建可取消Context方法,它有2个返回值,分别是Context类型和func()类型,Context(第一个返回值)在使用时一般会传给其他协程,第二个返回值放在main协程或顶级协程中处理,实现了调用方caller和被调方callee隔离。callee只管负责收到caller发送的取消信息时执行退出操作。
// canceler接口,核心是cancel方法,Done()不能省略,propagateCancel中的child.Done()
//在使用,因为Context接口中已有Done()方法了,它们的签名是一模一样的
// context包核心的两个接口是这里的canceler和前面的Context接口,为啥不把这里的canceler与
// Context合成一个接口呢?
// 1. 这里可以看到作者的设计思想,cancel操作不是Context必须功能,像

本文深入解析了Golang的Context包,探讨了为何需要Context,通过源码分析展示了Context如何实现goroutine的控制、超时和取消功能。文章还介绍了Context的最佳实践,强调了在多层协程结构和微服务中Context的重要性,并提供了相关场景的应用示例。
最低0.47元/天 解锁文章
1536





