go context

108 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 22 天,点击查看活动详情

context

Go语言中的context是一种标准库中提供的用于跨API边界和协程之间传递请求作用域的机制。它是一种数据结构,可以通过上下文传递请求相关的值,例如:请求ID、身份验证令牌、跟踪信息等。

context可以用于管理请求的生命周期,允许开发人员在代码中传递请求相关的值,同时不破坏代码结构。同时,它也可以帮助管理超时、取消和截止时间等操作。

在Go语言的标准库中,很多API都支持接收context作为参数,例如httpdatabase/sql等包,它们可以将传递的上下文中的信息进行跟踪或者记录,同时也可以根据上下文的超时或取消操作来取消正在进行的操作。

使用context的优点是能够避免在代码中使用全局变量或者在函数之间传递多个参数,从而保持代码的清晰性和可读性。

Context 基本使用方法

type Context interface {
   Deadline() (deadline time.Time, ok bool)
   Done() <-chan struct{}
   Err() error
   Value(key interface{}) interface{}
}
  • Deadline 方法会返回这个 Context 被取消的截止日期。如果没有设置截止日期,ok 的值是 false。
  • Done 方法返回一个 Channel 对象。在 Context 被取消时,此 Channel 会被 close,如果没被取消,可能会返回 nil。
  • Err() 如果 Done 没有被 close,Err 方法返回nil;如果 Done 被 close,Err 方法会返回 Done 被 close 的原因。
  • Value 返回此 ctx 中和指定的 key 相关联的 value。

context.Background():返回一个非 nil 的、空的 Context,没有任何值,不会被cancel,不会超时,没有截止日期。一般用在主函数、初始化、测试以及创建根Context 的时候。与context.TODO()一样。

使用 Context 的时候,有一些约定俗成的规则:

  1. 放在第一个参数的位置从来不把 nil 当做 Context 类型的参数值,可以使用 context.Background() 创建一个空的上下文对象,也不要使用 nil。
  2. Context 只用来临时做函数之间的上下文透传,不能持久化 Context 或者把 Context长久保存。把 Context 持久化到数据库、本地文件或者全局变量、缓存中都是错误的用法。
  3. key 的类型不应该是字符串类型或者其它内建类型,否则容易在包之间使用 Context 时候产生冲突。使用 WithValue 时,key 的类型应该是自己定义的类型。
  4. 常常使用 struct{}作为底层类型定义 key 的类型。对于 exported key 的静态类型,常常是接口或者指针。这样可以尽量减少内存分配。

传递信息 WithValue

WithValue 方法其实是创建了一个类型为 valueCtx 的 Context

type valueCtx struct {
   Context
   key, val any
}

func (c *valueCtx) Value(key any) any {
    if c.key == key {
        return c.val
    }
    return value(c.Context, key)
}

所以对key的查询是一层层向上查找。

// 创建一个父context
ctx := context.Background()

// 在父context上创建一个子context,并添加一些值
ctx = context.WithValue(ctx, "name", "John")
ctx = context.WithValue(ctx, "age", 30)

// 启动一个goroutine,并传递子context
go func(ctx context.Context) {
   // 在子context中读取添加的值
   name, ok := ctx.Value("name").(string)
   if !ok {
      fmt.Println("Failed to get name from context")
      return
   }

   age, ok := ctx.Value("age").(int)
   if !ok {
      fmt.Println("Failed to get age from context")
      return
   }

   // 输出结果
   fmt.Printf("Name: %s\nAge: %d\n", name, age)

}(ctx)

// 等待一段时间,确保goroutine有足够的时间执行
time.Sleep(time.Second)

WithCancel

WithCancel 方法返回 parent 的副本和一个函数,调用函数时相当于触发副本的Done,从而可以进行通知任务的中止。只要你的任务正常完成了,就需要调用cancel。

func worker(ctx context.Context) {
   for {
      select {
      default:
         fmt.Println("working")
         time.Sleep(1 * time.Second)
      case <-ctx.Done():
         fmt.Println("cancelled")
         return
      }
   }
}

func main() {
   ctx, cancel := context.WithCancel(context.Background())
   go worker(ctx)

   time.Sleep(5 * time.Second)
   cancel()
   time.Sleep(2 * time.Second)
   fmt.Println("done")
}

WithTimeout

底层调用的with Deadline:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
   return WithDeadline(parent, time.Now().Add(timeout))
}

功能就是时间到了,ctx.Done()就关闭。

func main() {
   // 创建一个带有超时控制的 context
   ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
   defer cancel()

   // 模拟一个需要超时控制的 IO 操作,每隔一秒输出一条日志
   // 直到 context 被取消或超时为止
   for {
      select {
      case <-ctx.Done():
         fmt.Println("IO operation cancelled or timed out")
         return
      default:
         fmt.Println("Performing IO operation...")
         time.Sleep(1 * time.Second)
      }
   }
}

WithDeadline

WithDeadline 会返回一个 parent 的副本,并且设置了一个不晚于参数 d 的截止时间,类型为 timerCtx(或者是 cancelCtx)。

如果它的截止时间晚于 parent 的截止时间,那么就以 parent 的截止时间为准,并返回一个类型为 cancelCtx 的 Context,因为 parent 的截止时间到了,就会取消这个cancelCtx。

如果当前时间已经超过了截止时间,就直接返回一个已经被 cancel 的 timerCtx。否则就会启动一个定时器,到截止时间取消这个 timerCtx。

func main() {
   // 创建一个父上下文,并设置截止时间为 5 秒后
   parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
   defer cancel()

   // 创建一个子上下文,并传入父上下文
   childCtx, cancel := context.WithCancel(parentCtx)
   defer cancel()

   // 在一个 goroutine 中处理工作,并传入子上下文
   go doWork(childCtx)

   // 主函数阻塞等待父上下文被取消或达到截止时间
   <-parentCtx.Done()

   // 检查父上下文的结束原因
   switch parentCtx.Err() {
   case context.DeadlineExceeded:
      fmt.Println("父上下文超时,工作无法完成")
   case context.Canceled:
      fmt.Println("父上下文被取消,工作已经停止")
   }
}

func doWork(ctx context.Context) {
   // 在 3 秒内完成工作
   select {
   case <-time.After(3 * time.Second):
      fmt.Println("工作已经完成")
   case <-ctx.Done():
      // 如果 ctx 被取消或者父上下文的截止时间已过,则停止工作
      fmt.Println("工作被取消或者超时")
   }
}

总结

Context 在 Go 中广泛应用于跨 goroutine 传递上下文信息,特别是在网络编程、并发编程、超时控制等方面。以下是几个常见的使用场景:

  1. 控制 goroutine 生命周期:使用 context.WithCancelcontext.WithTimeout 等函数可以很方便地控制 goroutine 的运行时间和结束时间,从而避免 goroutine 泄露和资源浪费。
  2. 处理 HTTP 请求超时:在处理 HTTP 请求时,我们可以使用 context.WithTimeoutcontext.WithDeadline 等函数设置请求超时时间,防止长时间等待造成资源浪费和系统阻塞。
  3. 处理并发任务:在处理并发任务时,我们可以使用 Context 在多个 goroutine 之间传递任务信息、传递取消信号和传递任务错误信息等,以协调多个 goroutine 之间的协作。
  4. 处理数据库事务:在处理数据库事务时,我们可以使用 Context 传递事务信息,以保证事务的完整性和一致性。
  5. 处理日志记录:在处理日志记录时,我们可以使用 Context 传递日志信息,以便在需要时可以快速地查询、分析和定位问题。