Exploring Context Package in Go: A Comprehensive Guide
Commitment is an act, not a word.
— Jean-Paul Sartre
1.1 context 包简介
从 context package versions - context - Go Packages我们可以知道 Go 语言在
go1.7beta1 版本的标准库中提供了context
包,它定义了Context
类型。
它是 goroutine 的上下文,可以用来传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。
我们可以理解为它是一个能承载程序运行的上下文。当然这只是一些基本的了解,go 官方有一句比较关键的话如下:
The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.
这句话我们初步可以看出context 和 gouroutinue 联系紧密,接下来让我们继续往下看。
1.2 为什么 context
众所周知,一个看似简单的业务请求在服务端是很复杂的。它的背后往往是多个goroutine 同时在工作,有的去redis获取元数据,有的去 s3 获取具体数据,有的去调用下游其他微服务接口等等。
这就要求请求之间有隶属关系,可以方便我们进行超时控制、回收资源等, 例如我们不希望当上游的请求停止,下游的gorooutine 还在执行任务,因为这会浪费资源、资源泄漏甚至是服务雪崩。
Go 语言中独特的设计 context 就是为了解决这种场景问题(在其他编程语言中我们很少见到类似的概念)。
通过context 和 goroutinue的配合,我们可以实现 Go Concurrency Patterns: Context - The Go Programming Language 该文中introduction 介绍的那样的效果。
When a request is canceled or times out, all the goroutines working on that request should exit quickly so the system can reclaim any resources they are using.
至于如何实现,我们可以在源码赏析中找到答案。
1.3 创建和使用 context
我们可以使用 context
包中的函数创建 context
:
1. context.Background()
和 context.TODO()
context.Background()
返回一个空的 context
,这个 context
不包含任何数据,不支持取消操作,并且没有设置超时时间。在主函数、初始化以及测试代码中,我们通常会使用 context.Background()
。
而 context.TODO()
同样返回一个空的 context
,但在语义上,我们会在不确定该使用哪种 context
时,选择使用 context.TODO()
。
1 | ctx := context.Background() |
WithCancel Example:
1 | package main |
WithDeadline、WithTimeout、WithValue 等使用可以参考官方示例 Context Examples
1.4 context 源码赏析
这里重点以 go1.22.4 中的 WithCancel为例进行分析
1 | func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { |
我们按执行步骤梳理如下:
- 创建一个
cancelCtx
对象,作为子context
- 然后调用
propagateCancel
构建父子context
之间的关联关系,这样当父context
被取消时,子context
也会被取消。 - 返回子
context
对象和子树取消函数
cancelCtx
类及 propagateCancel() 方法
1 | type cancelCtx struct { |
上述函数有与父上下文相关的三种不同的情况:
- 当
parent.Done() == nil
,也就是parent
不会触发取消事件时,当前函数会直接返回; - 当
child
的继承链包含可以取消的上下文时,会判断parent
是否已经触发了取消信号;- 如果已经被取消,
child
会立刻被取消; - 如果没有被取消,
child
会被加入parent
的children
列表中,等待parent
释放取消信号;
- 如果已经被取消,
- 当父上下文是开发者自定义的类型、实现了
context.Context
接口并在Done()
方法中返回了非空的管道时;- 运行一个新的 Goroutine 同时监听
parent.Done()
和child.Done()
两个 Channel; - 在
parent.Done()
关闭时调用child.cancel
取消子上下文;
- 运行一个新的 Goroutine 同时监听
context.propagateCancel
的作用是在 parent
和 child
之间同步取消和结束的信号,保证在 parent
被取消时,child
也会收到对应的信号,不会出现状态不一致的情况。
cancel() 方法
最重要的方法是 context.cancelCtx.cancel
,该方法会关闭上下文中的 Channel 并向所有的子上下文同步取消信号:
1 | func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) { |
通过源码我们可以知道cancel
方法可以被重复调用,是幂等的。综上,可以画个图简单表示cancel 的流程:
其他方法
context.WithDeadline
和 context.WithTimeout
逻辑类似,但是增加了定时器的处理。
它们在创建 context.timerCtx
的过程中判断了父上下文的截止日期与当前日期,并通过 time.AfterFunc
创建定时器,当时间超过了截止日期后会调用 context.timerCtx.cancel
同步取消信号。
而 XXXCause
进一步延展,增加了 error 相关的流程,解决了一些令人诟病的问题:错误返回不够友好,无法自定义错误,出现难以排查的问题时不好排查。
1 | ctx, cancel := context.WithCancelCause(parent) |
具体的例子有一篇文章已经给出:Context cancel cause in Go 1.20. Go 1.20 introduced two new functions in… | by Peter Gillich | Dev Genius
1.5 context 的最佳实践
这里有一些最佳实践可以参考:
不要把 context 存入结构体中: context 在设计时,就是为了方便在函数之间传递,而不是用来保存状态信息的。所以,尽量保持它的流动性,而不是将它存储在结构体中。
使用 context 的 Value 方法,应保持最小化: 尽管 context 的 Value 方法在一些场景下非常有用,但大部分情况下,如果滥用这个功能会导致代码更难维护,尤其是当 context 被大量使用的时候。
遵守 context 包的生命周期管理规则: 一般来说,context 应由发起请求的最外层函数创建,并通过调用链向下传递。具体地,如果你的函数需要在子进程中运行,并且可能需要被取消或超时,则应该考虑接收一个 context 参数。尽量不要在全局变量中存储和使用 context。
通过 defer 语句调用 cancel 函数: 当使用
WithCancel
、WithTimeout
或WithDeadline
函数返回的 cancel 函数时,你应该调用它们来释放相关资源。最佳实践是,在你的函数返回之前的地方,通过 defer 语句来调用这个 cancel 函数。尽量创建新的 context: 而不是复用已经存在的 context,这样可以让每个请求都有一个有明确生命周期的 context。
1.6 总结
Go 官方建议我们把 Context 作为函数的第一个参数,如果我们想控制所有的协程的取消动作,几乎所有的函数里都加上一个 Context 参数,context 将像病毒一样扩散的到处都是。
另外,像 WithCancel
等创建函数,实际上是创建了一个个的链表结点而已。我们知道,对链表的操作,通常都是 O(n)
复杂度的,而子节点多了,效率显然会变低。
那么,context 包到底解决了什么问题呢?答案是:cancelation
。它能帮助我们更好的做并发控制,能更好的管理goroutine
滥用。仅管它并不完美,但它确实很简洁地解决了问题。
1.7 参考
- context package versions - context - Go Packages
- Go Concurrency Patterns: Context - The Go Programming Language
- Context cancel cause in Go 1.20. Go 1.20 introduced two new functions in… | by Peter Gillich | Dev Genius
- segmentfault.com/a/1190000040917752
- 深度解密Go语言之context - Stefno - 博客园
- Go 语言并发编程与 Context | Go 语言设计与实现
更多该系列文章,参考medium链接:
https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a
English post: https://programmerscareer.com/golang-context/
作者:Wesley Wei – Twitter Wesley Wei – Medium
注意:原文在 2024-06-13 01:13 时发布于 https://programmerscareer.com/golang-context/. 本文为作者原创,转载请注明出处。
评论