什么是 Context

上下文 context.Context在Go 语言中用来设置截止日期、同步信号,传递请求相关值的结构体。
上下文与 Goroutine 有比较密切的关系,是 Go 语言中独特的设计,在其他编程语言中我们很少见到类似的概念。
主要用于超时控制和多Goroutine间的数据传递。

注:这里的数据传递主要指全局数据,如 链路追踪里的 traceId 之类的数据,并不是普通的参数传递(也非常不推荐用来传递参数)。

为什么需要 Context

WaitGroup 和信道(channel)是常见的 2 种并发控制的方式。
如果并发启动了多个子协程,需要等待所有的子协程完成任务,WaitGroup 非常适合于这类场景,例如下面的例子:

1. 利用全变量完成上下文控制

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. "time"
  6. )
  7. var wg sync.WaitGroup
  8. // 1. 利用全变量完成
  9. var stop bool
  10. func cpuInfo() {
  11. defer wg.Done()
  12. for {
  13. if stop {
  14. fmt.Println("退出cpu监控")
  15. break
  16. }
  17. time.Sleep(time.Second*2)
  18. fmt.Println("cpu信息读取完成")
  19. }
  20. }
  21. func main() {
  22. wg.Add(1)
  23. go cpuInfo()
  24. time.Sleep(time.Second*5)
  25. // 5秒后不再监控
  26. stop = true
  27. wg.Wait()
  28. fmt.Println("信息监控完成")
  29. }

2. 利用 chan

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. "time"
  6. )
  7. var wg sync.WaitGroup
  8. // 2. 利用 chan
  9. var stop2 chan bool = make(chan bool)
  10. func cpuInfo2() {
  11. defer wg.Done()
  12. for {
  13. select {
  14. case <- stop2:
  15. fmt.Println("退出cpu监控")
  16. return
  17. default:
  18. // 上面没有就执行下面
  19. time.Sleep(time.Second*2)
  20. fmt.Println("cpu信息读取完成")
  21. }
  22. }
  23. }
  24. func main() {
  25. wg.Add(1)
  26. go cpuInfo2()
  27. time.Sleep(time.Second*6)
  28. // 5秒后不再监控
  29. stop2 <- true
  30. wg.Wait()
  31. fmt.Println("信息监控完成")
  32. }

更复杂的场景如何做并发控制呢?比如子协程中开启了新的子协程,或者需要同时控制多个子协程。
这种场景下,select+chan的方式就显得力不从心了。

Go 语言提供了 Context 标准库可以解决这类场景的问题,Context 的作用和它的名字很像,上下文,即子协程的下上文。Context 有两个主要的功能:

  • 通知子协程退出(正常退出,超时退出等);
  • 传递必要的参数。

context整体概览

context 包的代码并不长,context.go 文件总共不到 500 行,其中还有很多大段的注释,代码可能也就 200 行左右的样子,是一个非常值得研究的代码库。

先给大家看一张整体的图:

类型 名称 作用
Context 接口 定义了 Context 接口的四个方法
emptyCtx 结构体 实现了 Context 接口,它其实是个空的 context
CancelFunc 函数 取消函数
canceler 接口 context 取消接口,定义了两个方法
cancelCtx 结构体 可以被取消
timerCtx 结构体 超时会被取消
valueCtx 结构体 可以存储 k-v 对
Background 函数 返回一个空的 context,常作为根 context
TODO 函数 返回一个空的 context,常用于重构时期,没有合适的 context 可用
WithCancel 函数 基于父 context,生成一个可以取消的 context
newCancelCtx 函数 创建一个可取消的 context
propagateCancel 函数 向下传递 context 节点间的取消关系
parentCancelCtx 函数 找到第一个可取消的父节点
removeChild 函数 去掉父节点的孩子节点
init 函数 包初始化
WithDeadline 函数 创建一个有 deadline 的 context
WithTimeout 函数 创建一个有 timeout 的 context
WithValue 函数 创建一个存储 k-v 对的 context

使用

  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "sync"
  6. "time"
  7. )
  8. var wgp3 sync.WaitGroup
  9. /**
  10. 为了解决 Context1 和 Context2中的问题 go 1.7 提供Context.WithCancel
  11. 使用上变得更加优雅
  12. */
  13. func cpuInfo3(ctx context.Context) {
  14. defer wgp3.Done()
  15. // go memoryInfo3(ctx)
  16. // ctx2, _ := context.WithCancel(context.Background()) // 一个新的ctx
  17. ctx2, _ := context.WithCancel(ctx) // 父子关系 父退出,子也会退出 链式取消 web开发中常用
  18. // 深层嵌套
  19. go memoryInfo3(ctx2)
  20. for {
  21. select {
  22. case <- ctx.Done():
  23. fmt.Println("退出cpu监控")
  24. return
  25. default:
  26. // 上面没有就执行下面
  27. time.Sleep(time.Second*2)
  28. fmt.Println("cpu信息读取完成")
  29. }
  30. }
  31. }
  32. func memoryInfo3(ctx context.Context) {
  33. defer wgp3.Done()
  34. for {
  35. select {
  36. case <- ctx.Done():
  37. fmt.Println("退出内存监控")
  38. return
  39. default:
  40. // 上面没有就执行下面
  41. time.Sleep(time.Second*2)
  42. fmt.Println("内存信息读取完成")
  43. }
  44. }
  45. }
  46. func main() {
  47. wgp3.Add(2)
  48. // 使用context.WithCancel(parent)函数,创建一个可取消的子Context
  49. // 函数返回值有两个:子Context Cancel 取消函数
  50. // context.Background() 返回一个空的Context
  51. ctx, cancel := context.WithCancel(context.Background())
  52. go cpuInfo3(ctx)
  53. // go memoryInfo3(ctx)
  54. time.Sleep(time.Second*6)
  55. // 6秒后不再监控
  56. cancel()
  57. wgp3.Wait()
  58. fmt.Println("信息监控完成")
  59. }

使用准则

context 包一开始就告诉了我们应该怎么用,不应该怎么用,这是应该被共同遵守的约定。

  1. 不要把Context放在结构体中,要以参数的方式传递
  2. 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
  3. 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
  4. Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
  5. Context是线程安全的,可以放心的在多个goroutine中传递

    使用场景

  • 超时控制
  • 错误取消
  • 跨 goroutine 数据同步
  • 防止 goroutine 泄漏

    缺点

参考