context
Context 包定义了上下文类型,该上下文类型跨越 API 边界和进程之间传递截止期限,取消信号和其他请求范围值。
对服务器的传入请求应创建一个 Context,对服务器的传出调用应接受 Context。它们之间的函数调用链必须传播 Context,可以用使用 WithCancel,WithDeadline,WithTimeout 或 WithValue 创建的派生上下文替换。当 Context 被取消时,从它派生的所有 Context 也被取消。
WithCancel,WithDeadline 和 WithTimeout 函数采用 Context(父级)并返回派生的 Context(子级)和 CancelFunc。调用 CancelFunc 将取消子对象及其子对象,删除父对子对象的引用,并停止任何关联的定时器。未能调用 CancelFunc 会泄漏子项及其子项,直至父项被取消或计时器激发。go vet 工具检查在所有控制流路径上使用 CancelFuncs。
使用 Contexts 的程序应该遵循这些规则来保持包之间的接口一致,并使静态分析工具能够检查上下文传播:
不要将上下文存储在结构类型中;相反,将一个 Context 明确地传递给每个需要它的函数。上下文应该是第一个参数,通常命名为 ctx:
1 | func DoSomething(ctx context.Context, arg Arg) error { |
即使函数允许,也不要传递 nil Context。如果您不确定要使用哪个 Context,请传递 context.TODO。
使用上下文值仅适用于传输进程和 API 的请求范围数据,而不用于将可选参数传递给函数。
相同的上下文可以传递给在不同 goroutine 中运行的函数; 上下文对于多个 goroutine 同时使用是安全的。
有关使用 Contexts 的服务器的示例代码,请参阅https://blog.golang.org/context。
Variables
Canceled
1 | var Canceled = errors.New("context canceled") |
Canceled is the error returned by Context.Err when the context is canceled.
DeadlineExceeded
1 | var DeadlineExceeded error = deadlineExceededError{} |
DeadlineExceeded is the error returned by Context.Err when the context’s deadline passes. 截止时间过后返回的错误
type CancelFunc
1 | type CancelFunc func() |
func WithCancel
1 | func WithCancel(parent Context) (ctx Context, cancel CancelFunc) |
WithCancel
函数用来创建一个可取消的context
,返回一个context
和一个CancelFunc
,调用CancelFunc
即可触发cancel
操作。
取消 Context 会释放与它相关的资源,所以只要完成在 Context 中运行的操作,代码就应该调用 cancel。
示例:
这个例子演示了如何使用可取消的 context 来防止 goroutine 泄漏。在示例函数结束时,由 gen 启动的 goroutine 将返回而不会泄漏。
1 | package main |
func WithDeadline
1 | func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) |
WithDeadline
返回一个基于parent
的可取消的context
,并且其过期时间deadline
不晚于所设置时间d
。
如果父节点parent
有过期时间并且过期时间早于给定时间d
,那么新建的子节点context
无需设置过期时间,使用WithCancel
创建一个可取消的context
即可;
示例:
1 | d := time.Now().Add(50 * time.Millisecond) |
func WithTimeout
与WithDeadline
类似,WithTimeout
也是创建一个定时取消的context
,只不过WithDeadline
是接收一个过期时间点,而WithTimeout
接收一个相对当前时间的过期时长timeout
:
1 | func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { |
type Context
context 包含截止日期,取消信号以及跨越 API 边界的其他值。
上下文的方法可能会被多个 goroutine 同时调用。
1 | type Context interface { |
Deadline
返回绑定当前context
的任务被取消的截止时间;如果没有设定期限,将返回ok == false
。Done
当绑定当前context
的任务被取消时,将返回一个关闭的channel
;如果当前context
不会被取消,将返回nil
。Err
如果Done
返回的channel
没有关闭,将返回nil
;如果Done
返回的channel
已经关闭,将返回非空的值表示任务结束的原因。如果是context
被取消,Err
将返回Canceled
;如果是context
超时,Err
将返回DeadlineExceeded
。Value
返回context
存储的键值对中当前key
对应的值,如果没有对应的key
,则返回nil
。
func Background
emptyCtx
是一个int
类型的变量,但实现了context
的接口。emptyCtx
没有超时时间,不能取消,也不能存储任何额外信息,所以emptyCtx
用来作为context
树的根节点。
1 | // An emptyCtx is never canceled, has no values, and has no deadline. It is not |
但我们一般不会直接使用emptyCtx
,而是使用由emptyCtx
实例化的两个变量,分别可以通过调用Background
和TODO
方法得到,这两个context
在实现上是一样的。
func TODO
Background
和 TODO
方法在某种层面上看其实也只是互为别名,两者没有太大的差别,不过 context.Background()
是上下文中最顶层的默认值,所有其他的上下文都应该从 context.Background()
演化出来。
我们应该只在不确定时使用 context.TODO()
,在多数情况下如果函数没有上下文作为入参,我们往往都会使用 context.Background()
作为起始的 Context
向下传递。
func WithValue
WithValue
用以向context
添加键值对:
1 | func WithValue(parent Context, key, val interface{}) Context { |
这里添加键值对不是在原context
结构体上直接添加,而是以此context
作为父节点,重新创建一个新的valueCtx
子节点,将键值对添加在子节点上,由此形成一条context
链。获取value
的过程就是在这条context
链上由尾部上前搜寻。
返回parent
的一个副本,调用该副本的 Value(key)方法将得到 val。这样我们不光将根节点原有的值保留了,还在子孙节点中加入了新的值,注意若存在 Key 相同,则会被覆盖。
注意:为了避免使用 context 的包之间发生冲突,key 必须具有可比性( comparable ),不应该是 string 或者其他任何内置类型。应该使用自定义类型。
总结
Go 语言中的 Context
的主要作用还是在多个 Goroutine 或者模块之间同步取消信号或者截止日期,用于减少对资源的消耗和长时间占用,避免资源浪费,虽然传值也是它的功能之一,但是这个功能我们还是很少用到。
在真正使用传值的功能时我们也应该非常谨慎,不能将请求的所有参数都使用 Context
进行传递,这是一种非常差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。
context
包通过构建树型关系的 Context,来达到上一层 Goroutine 能对传递给下一层 Goroutine 的控制。对于处理一个 Request 请求操作,需要采用context
来层层控制 Goroutine,以及传递一些变量来共享。
- Context 对象的生存周期一般仅为一个请求的处理周期。即针对一个请求创建一个 Context 变量(它为 Context 树结构的根);在请求处理结束后,撤销此 ctx 变量,释放资源。
- 每次创建一个 Goroutine,要么将原有的 Context 传递给 Goroutine,要么创建一个子 Context 并传递给 Goroutine。
- Context 能灵活地存储不同类型、不同数目的值,并且使多个 Goroutine 安全地读写其中的值。
- 当通过父 Context 对象创建子 Context 对象时,可同时获得子 Context 的一个撤销函数,这样父 Context 对象的创建环境就获得了对子 Context 将要被传递到的 Goroutine 的撤销权。
使用原则
- 不要把 Context 存在一个结构体当中,显式地传入函数。Context 变量需要作为第一个参数使用,一般命名为 ctx;
- 即使方法允许,也不要传入一个 nil 的 Context,如果你不确定你要用什么 Context 的时候传一个 context.TODO;
- 使用 context 的 Value 相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数;
- 同样的 Context 可以用来传递到不同的 goroutine 中,Context 在多个 goroutine 中是安全的;
- 在子 Context 被传递到的 goroutine 中,应该对该子 Context 的 Done 信道(channel)进行监控,一旦该信道被关闭(即上层运行环境撤销了本 goroutine 的执行),应主动终止对当前请求信息的处理,释放资源并返回。
通常,context.Background()
作为最顶层的 Context,在此基础上创建可撤销的 Context。