Context
Go在1.7的版本中才正式把Context 加入到标准库中。在这之前,很多Web 框架在定义自己的 handler 时,都会传递一个自定义的 Context,把客户端的信息和客户端的请求信息放入到 Context 中。Go 最初提供了 golang.org/x/net/context 库用来提供上下文信息,最终还是在Go1.7 中把此库提升到标准库 context 包中
为啥呢?这是因为,在 Go1.7 之前,有很多库都依赖 golang.org/x/net/context 中的Context 实现,这就导致 Go 1.7 发布之后,出现了标准库 Context 和golang.org/x/net/context 并存的状况。新的代码使用标准库 Context 的时候,没有办法使用这个标准库的Context 去调用日有的使用x/net/context 实现的方法
所以,在 Go1.9 中,还专门实现了一个叫做 type alias 的新特性,然后把 x/net/context 中的Context定义成标准库Context 的别名,以解决新旧 Context 类型冲突问题,你可以看-下下面这段代码:
packagecontext
import "context"
type Context = context.Context
type CancelFunc = context.CancelFunc
Go标准库的 Context 不仅提供了上下文传递的信息,还提供了 cancel、timeout 等其它信息,这些信息貌似和 context 这个包名没关系,但是还是得到了广泛的应用。所以,你看context 包中的 Context 不仅仅传递上下文信息,还有 timeout 等其它功能,是不是“名不副实”呢?
Go的开发者也注意到了“关于 Context,存在一些争议”这件事儿,所以,Go核心开发者lan Lance Taylor 专门开了一个@issue 28342,用来记录当前的 Context 的问题
- Context 包名导致使用的时候重复 ctx context.Context;
- Context.WithValue 可以接受任何类型的值,非类型安全;
- Context 包名容易误导人,实际上,Context 最主要的功能是取消 goroutine 的执行
- Context 漫天飞,函数污染。
尽管有很多的争议,但是,在很多场景下,使用Context 其实会很方便,所以现在它已经在Go生态圈中传播开来了,包括很多的 Web 应用框架,都切换成了标准库的Context。标准库中的 database/sql、os/exec、net、net/http 等包中都使用到了 Context。而且,如果我们遇到了下面的一些场景,也可以考虑使用 Context:
- 上下文信息传递(request-scoped),比如处理http 请求在请求处理链路上传递信息;
- 控制子goroutine的运行
- 超时控制的方法调用;
- 可以取消的方法调用
所以,我们需要掌握Context 的具体用法,这样才能在不影响主要业务流程实现的时候,实现一些通用的信息传递,或者是能够和其它goroutine 协同工作,提供timeout、cancel等机制。
Context 基本使用方法
首先,我们来学习一下 Context 接口包含哪些方法,这些方法都是干什么用的。 包 context 定义了 Context 接口,Context 的具体实现包括 4 个方法,分别是 Deadline.Done、Err 和 Value,如下所示
type Context interface {
Deadline() (deadline time.Time,ok bool)
Done() <-chan struct{}
Err() error
Value(key interfacef}) interface{}
}
下面我来具体解释下这4个方法
Deadline 方法会返回这个 Context 被取消的截止日期。如果没有设置截止日期,ok 的值是false。后续每次调用这个对象的 Deadline 方法时,都会返回和第一次调用相同的结果。
Done 方法返回一个 Channel对象。在 Context 被取消时,此Channel 会被 close,如果没被取消,可能会返回 nil。后续的 Done 调用总是返回相同的结果。当 Done 被 cose 的时候,你可以通过 ctx.Err 获取错误信息。Done 这个方法名其实起得并不好,因为名字太过笼统,不能明确反映 Done 被 close 的原因,因为 cancel、timeout、deadline 都可能导致Done 被 close,不过,目前还没有一个更合适的方法名称。
关于 Done 方法,你必须要记住的知识点就是: 如果 Done 没有被 close,Err 方法返回 nil:如果 Done 被 close,Err 方法会返回 Done 被 close 的原因
Value 返回此 ctx 中和指定的 key 相关联的 value。
Context 中实现了2个常用的生成页层 Context 的方法。
-
context.Background(): 返回一个非 nil 的、空的 Context,没有任何值,不会被cancel,不会超时,没有截止日期。一般用在主函数、初始化、测试以及创建根 Context的时候。
-
context.TODO():返回一个非nil的、空的 Context,没有任何值,不会被 cancel,不会超时,没有截止日期。当你不清楚是否该用 Context,或者目前还不知道要传递一些什么上下文信息的时候,就可以使用这个方法。
官方文档是这么讲的,你可能会觉得像没说一样,因为界限并不是很明显。其实,你根本不用费脑子去考虑,可以直接使用 context.Background。事实上,它们两个底层的实现是一模一样的。
在使用Context的时候,有一些约定俗成的规则。
- 一般函数使用Context 的时候,会把这个参数放在第一个参数的位置
- 从来不把 nil 当做 Context 类型的参数值,可以使用 context.Background0 创建一个空的上下文对象,也不要使用 nil。
- Context 只用来临时做函数之间的上下文透传,不能持化 Context 或者把 Context 长久保存。把 Context 持久化到数据库、本地文件或者全局变量、缓存中都是错误的用法。
- key 的类型不应该是字符串类型或者其它内建类型,,否则容易在包之间使用Context 时候产生冲突。使用WithValue 时,key 的类型应该是自己定义的类型
- 常常使用struct作为底层类型定义 key 的类型。对于exported key 的静态类型,常常是接口或者指针。这样可以尽量减少内存分配。
Context 的其他方法
接下来,我会介绍标准库中几种创建特殊用途 Context 的方法: WithValue、WithCancel.WithTimeout 和 WithDeadline,包括它们的功能以及实现方式
WithValue
WithValue 基于 parent Context 生成一个新的 Context,保存了一个 key-value 键值对。常常用来传递上下文 WithValue 方法其实是创建了一个类型为 valueCtx的 Context,它的类型定义如下:
type valueCtx struct {
Context
key, val interface{}
}
它持有一个 key-value 键值对,还持有 parent 的 Context。它覆盖了 Value 方法,优先从自己的存储中检查这个 key,不存在的话会从 parent 中继续检查。
Go 标准库实现的 Context 还实现了链式查找。如果不存在,还会向 parent Context 去查找,如果 parent 还是 valueCtx 的话,还是遵循相同的原则: valueCtx 会嵌入 parent,所以还是会查找 parent的 Value 方法的。
ctx = context.TODO()
ctx = context.withValue(ctx, "key1", "001")
ctx = context.withValue(ctx, "key2", "002")
ctx = context.withValue(ctx, "key3", "003")
ctx = context.withValue(ctx, "key4", "004")
fmt.PrintIn(ctx.Value("key1"))
WithCancel
WithCancel 方法返回 parent 的副本,只是副本中的 Done Channel 是新建的对象,它的类型是 cancelCtx。 我们常常在一些需要主动取消长时间的任务时,创建这种类型的 Context,然后把这个Context 传给长时间执行任务的 goroutine。当需要中止任务时,我们就可以 cancel 这个Context,这样长时间执行任务的 goroutine,就可以通过检查这个 Context,知道 Context已经被取消了。
WithCancel 返回值中的第二个值是一个 cancel 函数。其实,这个返回值的名称 (cancel)和类型(Cancel)也非常迷惑人。 记住,不是只有你想中途放弃,才去调用 cancel,只要你的任务正常完成了,就需要调用cancel,这样,这个 Context 才能释放它的资源 (通知它的 children 处理 cancel,从它的parent 中把自己移除,甚至释放相关的 goroutine)。很多同学在使用这个方法的时候,都会忘记调用 cancel,切记切记,而且一定尽早释放。
func WithCancel(parent Context) (ctx Context,cancel CancelFunc) {
c := newCancelCtx(parent)propagateCancel(parent,&c)// 把c朝上传播
return &c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtxfContext: parent
}
代码中调用的 propagateCancel 方法会顺着 parent 路径往上找,直到找到一个cancelCtx,或者为 nil。如果不为空,就把自己加入到这个 cancelCtx 的 child,以便这个cancelCtx 被取消的时候通知自己。如果为空,会新起一个 goroutine,由它来监听 parent的 Done 是否已关闭。
当这个 cancelCtx的 cancel 函数被调用的时候,或者 parent 的 Done 被 close 的时候,这个 cancelCtx的 Done 才会被 close。
cancel 是向下传递的,如果一个 WithCancel 生成的 Context 被 cancel 时,如果它的子Context (也有可能是孙,或者更低,依赖子的类型)也是 cancelCtx 类型的,就会被cancel,但是不会向上传递。parent Context 不会因为子 Context 被 cancel 而 cancel。
cancelCtx 被取消时,它的 Err 字段就是下面这个 Canceled 错误:var Canceled = errors.New("context canceled")
WithTimeout
WithTimeout 其实是和 WithDeadline 一样,只不过一个参数是超时时间,一个参数是截止时间。超时时间加上当前时间,其实就是截止时间。
WithDeadline
WithDeadline 会返回一个 parent 的副本,并且设置了一个不晚于参数 d 的截止时间,类型为 timerCtx(或者是 cancelCtx)。 如果它的截止时间晚于 parent 的截止时间,那么就以 parent 的截止时间为准,并返回一个类型为 cancelCtx的 Context,因为 parent 的截止时间到了,就会取消这个 cancelCtx。
如果当前时间已经超过了截止时间,就直接返回一个已经被 cancel的 timerCtx。否则就会启动一个定时器,到截止时间取消这个 timerCtx。
综合起来,timerCtx的 Done 被 Close 掉,主要是由下面的某个事件触发的:
- 截止时间到了
- cancel函数被调用;
- parent的 Done 被 close.