【导读】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 基本使用方法

  1. type Context interface {
  2.     Deadline() (deadline time.Time, ok bool)
  3.     Done() <-chan struct{}
  4.     Err() error
  5.     Value(key interface{}) interface{}
  6. }

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,或者目前还不知道要传递一些什么上下文信息的时候,就可以使用这个方法;

  1. var (
  2.     background = new(emptyCtx)
  3.     todo       = new(emptyCtx)
  4. )
  5. func Background() Context {
  6.     return background
  7. }
  8. func TODO() Context {
  9.     return todo
  10. }

约定

一般函数使用 Context 的时候,会把这个参数放在第一个参数的位置。

从来不把 nil 当做 Context 类型的参数值,可以使用 context.Background() 创建一个空的上下文对象,也不要使用 nil。

Context 只用来临时做函数之间的上下文透传,不能持久化 Context 或者把 Context 长久保存。把 Context 持久化到数据库、本地文件或者全局变量、缓存中都是错误的用法。

key 的类型不应该是字符串类型或者其它内建类型,否则容易在包之间使用 Context 时候产生冲突。使用 WithValue 时,key 的类型应该是自己定义的类型。

常常使用 struct{}作为底层类型定义 key 的类型。对于 exported key 的静态类型,常常是接口或者指针。这样可以尽量减少内存分配

创建特殊用途 Context 的方法

WithValue

  1. type valueCtx struct {
  2.     Context
  3.     keyval interface{}
  4. }

Go 标准库实现的 Context 还实现了链式查找。如果不存在,还会向 parent Context 去查找,如果 parent 还是 valueCtx 的话,还是遵循相同的原则:valueCtx 会嵌入 parent,所以还是会查找 parent 的 Value 方法的;

  1. ctx = context.TODO()
  2. ctx = context.WithValue(ctx, "key1""0001")
  3. ctx = context.WithValue(ctx, "key2""0001")
  4. ctx = context.WithValue(ctx, "key3""0001")
  5. ctx = context.WithValue(ctx, "key4""0004")
  6. fmt.Println(ctx.Value("key1"))

WithCancel

cancel 是向下传递的,如果一个 WithCancel 生成的 Context 被 cancel 时,如果它的子 Context(也有可能是孙,或者更低,依赖子的类型)也是 cancelCtx 类型的,就会被 cancel,但是不会向上传递。parent Context 不会因为子 Context 被 cancel 而 cancel;

  1. func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
  2.     c := newCancelCtx(parent)
  3.     propagateCancel(parent, &c)// 把 c 朝上传播
  4.     return &cfunc() { c.cancel(trueCanceled) }
  5. }
  6. // newCancelCtx returns an initialized cancelCtx.
  7. func newCancelCtx(parent Context) cancelCtx {
  8.     return cancelCtx{Context: parent}
  9. }

WithTimeout

WithTimeout 其实是和 WithDeadline 一样,只不过一个参数是超时时间,一个参数是截止时间。超时时间加上当前时间,其实就是截止时间

  1. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
  2.     // 当前时间+timeout 就是 deadline
  3.     return WithDeadline(parent, time.Now().Add(timeout))
  4. }

WithDeadline

WithDeadline 会返回一个 parent 的副本,并且设置了一个不晚于参数 d 的截止时间,类型为 timerCtx(或者是 cancelCtx)。如果它的截止时间晚于 parent 的截止时间,那么就以 parent 的截止时间为准,并返回一个类型为 cancelCtx 的 Context,因为 parent 的截止时间到了,就会取消这个 cancelCtx

  1. func WithDeadline(parent Context, d time.Time) (ContextCancelFunc) {
  2.     // 如果 parent 的截止时间更早,直接返回一个 cancelCtx 即可
  3.     if cur, ok := parent.Deadline(); ok && cur.Before(d) {
  4.         return WithCancel(parent)
  5.     }
  6.     c := &timerCtx{
  7.         cancelCtx: newCancelCtx(parent),
  8.         deadline:  d,
  9.     }
  10.     propagateCancel(parent, c// 同 cancelCtx 的处理逻辑
  11.     dur := time.Until(d)
  12.     if dur <= 0 { //当前时间已经超过了截止时间,直接 cancel
  13.         c.cancel(trueDeadlineExceeded)
  14.         return cfunc() { c.cancel(falseCanceled) }
  15.     }
  16.     c.mu.Lock()
  17.     defer c.mu.Unlock()
  18.     if c.err == nil {
  19.         // 设置一个定时器,到截止时间后取消
  20.         c.timer = time.AfterFunc(dur, func() {
  21.             c.cancel(trueDeadlineExceeded)
  22.         })
  23.     }
  24.     return cfunc() { c.cancel(trueCanceled) }
  25. }

WithDeadline(WithTimeout)返回的 cancel 一定要调用,并且要尽可能早地被调用,这样才能尽早释放资源,不要单纯地依赖截止时间被动取消

  1. func slowOperationWithTimeout(ctx context.Context) (Result, error) {
  2.   ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
  3.   defer cancel() // 一旦慢操作完成就立马调用 cancel
  4.   return slowOperation(ctx)
  5. }

总结

使用 Context 来取消一个 goroutine 的运行,这是 Context 最常用的场景之一,Context 也被称为 goroutine 生命周期范围(goroutine-scoped)的 Context;

  1. func main() {
  2.     ctx, cancel := context.WithCancel(context.Background())
  3.     go func() {
  4.         defer func() {
  5.             fmt.Println("goroutine exit")
  6.         }()
  7.         for {
  8.             select {
  9.             case <-ctx.Done():
  10.                 return
  11.             default:
  12.                 time.Sleep(time.Second)
  13.             }
  14.         }
  15.     }()
  16.     time.Sleep(time.Second)
  17.     cancel()
  18.     time.Sleep(2 * time.Second)
  19. }

有时候,Context 并不会减少对服务器的请求负担。如果在 Context 被 cancel 的时候,你能关闭和服务器的连接,中断和数据库服务器的通讯、停止对本地文件的读写,那么,这样的超时处理,同时能减少对服务调用的压力,但是这依赖于你对超时的底层处理机制。

点赞(1420)

评论列表共有 0 条评论

立即
投稿
返回
顶部