【导读】Go 语言编程中的 Context 怎么用?如何更好更快地利用 Context 实现功能?本文做了详细介绍。
Context 的来历
Go 在 1.7 的版本中才正式把 Context 加入到标准库中。在这之前,很多 Web 框架在定义自己的 handler 时,都会传递一个自定义的 Context,把客户端的信息和客户端的请求信息放入到 Context 中;
使用场景
上下文信息传递 (request-scoped),比如处理 http 请求、在请求处理链路上传递信息;
控制子 goroutine 的运行;
超时控制的方法调用;
可以取消的方法调用
存在问题
当前的 Context 的问题:
Context 包名导致使用的时候重复 ctx context.Context;
Context.WithValue 可以接受任何类型的值,非类型安全;
Context 包名容易误导人,实际上,Context 最主要的功能是取消 goroutine 的执行;
Context 漫天飞,函数污染
Context 基本使用方法
- type Context interface {
- Deadline() (deadline time.Time, ok bool)
- Done() <-chan struct{}
- Err() error
- Value(key interface{}) interface{}
- }
Deadline
Deadline 方法会返回这个 Context 被取消的截止日期。如果没有设置截止日期,ok 的值是 false。后续每次调用这个对象的 Deadline 方法时,都会返回和第一次调用相同的结果;
Done
Done 方法返回一个 Channel 对象。在 Context 被取消时,此 Channel 会被 close,如果没被取消,可能会返回 nil。后续的 Done 调用总是返回相同的结果。当 Done 被 close 的时候,你可以通过 ctx.Err 获取错误信息。Done 这个方法名其实起得并不好,因为名字太过笼统,不能明确反映 Done 被 close 的原因,因为 cancel、timeout、deadline 都可能导致 Done 被 close,不过,目前还没有一个更合适的方法名称;
关于 Done 方法:如果 Done 没有被 close,Err 方法返回 nil;如果 Done 被 close,Err 方法会返回 Done 被 close 的原因;
Value
Value 返回此 ctx 中和指定的 key 相关联的 value。
Context 中实现了 2 个常用的生成顶层 Context 的方法。
context.Background():返回一个非 nil 的、空的 Context,没有任何值,不会被 cancel,不会超时,没有截止日期。一般用在主函数、初始化、测试以及创建根 Context 的时候。
context.TODO():返回一个非 nil 的、空的 Context,没有任何值,不会被 cancel,不会超时,没有截止日期。当你不清楚是否该用 Context,或者目前还不知道要传递一些什么上下文信息的时候,就可以使用这个方法;
- var (
- background = new(emptyCtx)
- todo = new(emptyCtx)
- )
-
- func Background() Context {
- return background
- }
-
- func TODO() Context {
- return todo
- }
约定
一般函数使用 Context 的时候,会把这个参数放在第一个参数的位置。
从来不把 nil 当做 Context 类型的参数值,可以使用 context.Background() 创建一个空的上下文对象,也不要使用 nil。
Context 只用来临时做函数之间的上下文透传,不能持久化 Context 或者把 Context 长久保存。把 Context 持久化到数据库、本地文件或者全局变量、缓存中都是错误的用法。
key 的类型不应该是字符串类型或者其它内建类型,否则容易在包之间使用 Context 时候产生冲突。使用 WithValue 时,key 的类型应该是自己定义的类型。
常常使用 struct{}作为底层类型定义 key 的类型。对于 exported key 的静态类型,常常是接口或者指针。这样可以尽量减少内存分配
创建特殊用途 Context 的方法
WithValue
- type valueCtx struct {
- Context
- key, val interface{}
- }
Go 标准库实现的 Context 还实现了链式查找。如果不存在,还会向 parent Context 去查找,如果 parent 还是 valueCtx 的话,还是遵循相同的原则:valueCtx 会嵌入 parent,所以还是会查找 parent 的 Value 方法的;
- ctx = context.TODO()
- ctx = context.WithValue(ctx, "key1", "0001")
- ctx = context.WithValue(ctx, "key2", "0001")
- ctx = context.WithValue(ctx, "key3", "0001")
- ctx = context.WithValue(ctx, "key4", "0004")
-
- fmt.Println(ctx.Value("key1"))
WithCancel
cancel 是向下传递的,如果一个 WithCancel 生成的 Context 被 cancel 时,如果它的子 Context(也有可能是孙,或者更低,依赖子的类型)也是 cancelCtx 类型的,就会被 cancel,但是不会向上传递。parent Context 不会因为子 Context 被 cancel 而 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 cancelCtx{Context: parent}
- }
WithTimeout
WithTimeout 其实是和 WithDeadline 一样,只不过一个参数是超时时间,一个参数是截止时间。超时时间加上当前时间,其实就是截止时间
- func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
- // 当前时间+timeout 就是 deadline
- return WithDeadline(parent, time.Now().Add(timeout))
- }
WithDeadline
WithDeadline 会返回一个 parent 的副本,并且设置了一个不晚于参数 d 的截止时间,类型为 timerCtx(或者是 cancelCtx)。如果它的截止时间晚于 parent 的截止时间,那么就以 parent 的截止时间为准,并返回一个类型为 cancelCtx 的 Context,因为 parent 的截止时间到了,就会取消这个 cancelCtx
- func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
- // 如果 parent 的截止时间更早,直接返回一个 cancelCtx 即可
- if cur, ok := parent.Deadline(); ok && cur.Before(d) {
- return WithCancel(parent)
- }
- c := &timerCtx{
- cancelCtx: newCancelCtx(parent),
- deadline: d,
- }
- propagateCancel(parent, c) // 同 cancelCtx 的处理逻辑
- dur := time.Until(d)
- if dur <= 0 { //当前时间已经超过了截止时间,直接 cancel
- c.cancel(true, DeadlineExceeded)
- return c, func() { c.cancel(false, Canceled) }
- }
- c.mu.Lock()
- defer c.mu.Unlock()
- if c.err == nil {
- // 设置一个定时器,到截止时间后取消
- c.timer = time.AfterFunc(dur, func() {
- c.cancel(true, DeadlineExceeded)
- })
- }
- return c, func() { c.cancel(true, Canceled) }
- }
WithDeadline(WithTimeout)返回的 cancel 一定要调用,并且要尽可能早地被调用,这样才能尽早释放资源,不要单纯地依赖截止时间被动取消
- func slowOperationWithTimeout(ctx context.Context) (Result, error) {
- ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
- defer cancel() // 一旦慢操作完成就立马调用 cancel
- return slowOperation(ctx)
- }
总结
使用 Context 来取消一个 goroutine 的运行,这是 Context 最常用的场景之一,Context 也被称为 goroutine 生命周期范围(goroutine-scoped)的 Context;
- func main() {
- ctx, cancel := context.WithCancel(context.Background())
-
- go func() {
- defer func() {
- fmt.Println("goroutine exit")
- }()
-
- for {
- select {
- case <-ctx.Done():
- return
- default:
- time.Sleep(time.Second)
- }
- }
- }()
-
- time.Sleep(time.Second)
- cancel()
- time.Sleep(2 * time.Second)
- }
有时候,Context 并不会减少对服务器的请求负担。如果在 Context 被 cancel 的时候,你能关闭和服务器的连接,中断和数据库服务器的通讯、停止对本地文件的读写,那么,这样的超时处理,同时能减少对服务调用的压力,但是这依赖于你对超时的底层处理机制。
发表评论 取消回复