前言
在写Golang
程序调用各种第三方库的时候, 经常会传一个叫做Context
的参数. 之前基本上见到接Context
, 根本不管是干什么用的, 直接无脑context.Background()
.
但是, 传着传着就不免发生一些小疑问, 这个参数到底是干什么用的呢? 这么多库都在使用, 至少说明其是Golang
中的一个共识, 一个基础元素. 除了context.Background()
一定还有其他的值, 否则也不会作为参数来接收了.
用了这么久, 都不知道它的作用, 这实在有点说不过去了, 于是抽时间来好好研究研究context
在Golang
中的作用.
作用
在Golang
源码包中, context.go
很贴心的给出来官网介绍的文章: https://blog.golang.org/context
在Golang
中, 协程被设计为非常方便且轻量的用户态线程, 鼓励我们使用协程来完成各种耗时的操作.
假设, 有一个处理请求A
的goroutine
, 其同时又启动了n
个额外的goroutine
来协助操作. 此时, 若请求 A
被取消, 那么所有相关的资源都需要被释放, 所有处理该请求的goroutine
都应该快速退出, 防止造成资源的浪费. 那么问题来了, parent
是无法主动关闭子协程的, 如何通知子协程需要提前退出呢?
我想了想, 这不就是多协程之间的通信么, 这块我熟啊. 用锁, 用管道, 用什么都行, 只要将取消的消息群发给所有子协程就可以了, 随便列举几个方案:
- 共享变量. 通过共享一个
bool
变量, 子协程定期检测变量值来判断是否需要退出 - 管道. 子协程定期检测管道是否关闭来判断是否需要退出
- 等等
没错了, Context
就是干这个用的. 它提出来的主要目的, 就是提醒子协程该退出了. 而它实现的原理也是通过协程通信实现的, 只是在其上面封了一层, 以方便调用.
虽然Context
在刚开始的时候只是为了封装子协程的退出提醒(只是我的猜测), 但是既然都加上下文了, 自然得带点上下文环境变量了. 其作用通过Context
接口暴露的方法可窥见一二:
Deadline() (deadline time.Time, ok bool)
- 返回工作的截止时间. (若没有, 则 ok=false)
Done() <-chan struct{}
- 返回一个管道. 若从管道中可以读到数据, 说明任务被关闭了. (若不需要, 返回 nil)
- 通常做法为在需要退出时将管道关闭.
Err() error
- 返回任务被关闭的原因.
Value(key any) any
- 返回上下文包含的环境变量
因此, Context
其实只能够干两件事
- 通知子协程提前退出
- 携带环境变量
context 包
Golang
的context
包除了测试文件, 仅contxt.go
一个文件. 且, 将context.go
文件中的注释和空行去掉之后, 整个文件代码仅318行(使用版本为 go 1.18), 称得上短小精悍了. 这里就不分析源码了, 感兴趣的自己看一下, 简单整理一些context
包中包含的内容:
官方已实现的常用Context
结构体, 均为私有结构体, 需要使用下方的方法生成, 官方仅实现了常用的几个实例, 这几个实例可组合使用, 若不能满足自己需求也可以自己实现:
名称 | 作用 |
---|---|
emptyCtx | 空的Context , 所有接口实现均为空实现. 既永远不会停止 |
cancelCtx | 可以被主动取消的Context . parent 调用特定方法进行取消 |
timerCtx | 定时器, 在指定时间后取消 |
valueCtx | 永远不会停止, 用来传递上下文环境变量 |
context
包中提供的公共方法, 我们通常调用的就是这些了, 可以看到, 官方提供的生成方法, 都是基于一个已有的context
基础上进行生成, 也就是说我们在使用的时候可以进行组合, 将前一个生成的结果作为参数来生成下一个, 以获得拥有多种功能的context
, 这个设计还是很巧妙的:
类型 | 名称 | 作用 |
---|---|---|
方法 | Background | 返回一个emptyCtx 实例 |
TODO | 返回一个emptyCtx 实例. 官方推荐是, 当不知道应该用哪个的时候, 临时使用 TODO |
|
WithCancel | 在一个context 的基础上, 生成一个可以取消的context . 两个 context 均可以执行取消的操作, 取决于哪个先发生 |
|
WithDeadline | 在一个context 基础上, 生成一个特定时间关闭的context |
|
WithTimeout | 在一个context 基础上, 生成一个特定时长关闭的context |
|
WithValue | 在一个context 基础上, 生成一个拥有指定k-v 的context |
|
变量 | Canceled | 定义好的取消文案 |
DeadlineExceeded | 定义好的取消文案. 时间到了 |
最后
最后, 在查看了Context
内容之后, 有些个人的小疑问.
1. Context
接口设计的过于臃肿
其实, Context
并不需要保存k-v
, 应用程序总是可以通过自己的方式将上下文在多个协程之间共享的.
并且, Deadline
和Done
方法是否有些臃肿? 其实子协程仅仅需要知道是否退出即可, 甚至于我觉得Context
只需要实现这样一个方法就可以了:
IsCancel() (cancel bool, e err)
十分简洁, 只需要告诉子协程是否退出及原因不就可以了么?
当然, 也可能官方是出于其他我没有想到的原因考虑.
2. parent
只能通知子协程退出, 但子协程什么时候退出parent
是不知道的
如题, parent
通过Context
通知子协程退出后, 对于子协程是否退出是没有感知的. 现实中是否有这样的场景暂时没有想到, 不过应该会有吧.