context 包的代码并不长,context.go
文件总共不到 500 行,其中还有很多大段的注释,代码可能也就 200 行左右的样子,是一个非常值得研究的代码库。
先看一张图:
类型 | 名称 | 作用 |
---|---|---|
Context | 接口 | 定义了 Context 接口的四个方法 |
emptyCtx | 结构体 | 实现了 Context 接口,它其实是个空的 context |
CancelFunc | 函数 | 取消函数 |
canceler | 接口 | context 取消接口,定义了两个方法 |
cancelCtx | 结构体 | 可以被取消 |
timerCtx | 结构体 | 超时会被取消 |
valueCtx | 结构体 | 可以存储 k-v 对 |
Background | 函数 | 返回一个空的 context,常作为根 context |
TODO | 函数 | 返回一个空的 context,常用于重构时期,没有合适的 context 可用 |
WithCancel | 函数 | 基于父 context,生成一个可以取消的 context |
newCancelCtx | 函数 | 创建一个可取消的 context |
propagateCancel | 函数 | 向下传递 context 节点间的取消关系 |
parentCancelCtx | 函数 | 找到第一个可取消的父节点 |
removeChild | 函数 | 去掉父节点的孩子节点 |
init | 函数 | 包初始化 |
WithDeadline | 函数 | 创建一个有 deadline 的 context |
WithTimeout | 函数 | 创建一个有 timeout 的 context |
WithValue | 函数 | 创建一个存储 k-v 对的 context |
上面这张表展示了 context 的所有函数、接口、结构体,可以纵览全局,可以在读完文章后,再回头细看。
整体类图如下:
接口
Context
现在可以直接看源码:
type Context interface {
// 当 context 被取消或者到了 deadline,返回一个被关闭的 channel
Done() <-chan struct{
}
// 在 channel Done 关闭后,返回 context 取消原因
Err() error
// 返回 context 是否会被取消以及自动取消时间(即 deadline)
Deadline() (deadline time.Time, ok bool)
// 获取 key 对应的 value
Value(key interface{
}) interface{
}
}
Context
是一个接口,定义了 4 个方法,它们都是幂等
的。也就是说连续多次调用同一个方法,得到的结果都是相同的。
Done()
返回一个 channel,可以表示 context 被取消的信号:当这个 channel 被关闭时,说明 context 被取消了。注意,这是一个只读的channel。 我们又知道,读一个关闭的 channel 会读出相应类型的零值。并且源码里没有地方会向这个 channel 里面塞入值。换句话说,这是一个 receive-only
的 channel。因此在子协程里读这个 channel,除非被关闭,否则读不出来任何东西。也正是利用了这一点,子协程从 channel 里读出了值(零值)后,就可以做一些收尾工作,尽快退出。
Err()
返回一个错误,表示 channel 被关闭的原因。例如是被取消,还是超时。
Deadline()
返回 context 的截止时间,通过此时间,函数就可以决定是否进行接下来的操作,如果时间太短,就可以不往下做了,否则浪费系统资源。当然,也可以用这个 deadline 来设置一个 I/O 操作的超时时间。
Value()
获取之前设置的 key 对应的 value。
canceler
再来看另外一个接口:
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{
}
}
实现了上面定义的两个方法的 Context,就表明该 Context 是可取消的。源码中有两个类型实现了 canceler 接口:*cancelCtx
和 *timerCtx
。注意是加了 *
号的,是这两个结构体的指针实现了 canceler 接口。
Context 接口设计成这个样子的原因:
- “取消”操作应该是建议性,而非强制性
caller 不应该去关心、干涉 callee 的情况,决定如何以及何时 return 是 callee 的责任。caller 只需发送“取消”信息,callee 根据收到的信息来做进一步的决策,因此接口并没有定义 cancel 方法。
- “取消”操作应该可传递
“取消”某个函数时,和它相关联的其他函数也应该“取消”。因此,Done()
方法返回一个只读的 channel,所有相关函数监听此 channel。一旦 channel 关闭,通过 channel 的“广播机制”,所有监听者都能收到。
结构体
emptyCtx
源码中定义了 Context
接口后,并且给出了一个实现:
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 interface{
}) interface{
} {
return nil
}
看这段源码,非常 happy。因为每个函数都实现的异常简单,要么是直接返回,要么是返回 nil。
所以,这实际上是一个空的 context,永远不会被 cancel,没有存储值,也没有 deadline。
它被包装成:
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
通过下面两个导出的函数(首字母大写)对外公开:
func Background() Context {
return background
}
func TODO