context简单入门
在开发高效且可维护的 Go 应用程序时,处理超时、取消操作和传递请求范围的数据变得至关重要。
这时,Go 标准库中的 context
包就显得尤其重要了,它提供了在不同 API 层级之间传递取消信号、超时时间、截止日期,以及其他特定请求的值的能力。
简介
context
context
包允许你传递可取消的信号、超时时间、截止日期,以及跨 API 边界的请求范围的数据。在并发编程中,它非常有用,尤其是处理那些可能需要提前停止的长时间运行操作。
特性
Go 语言的 context
包提供了一系列方法,用于创建上下文(Context),这些上下文可以帮助我们管理请求的生命周期、取消信号、超时、截止日期和传递请求范围的数据。
官方建议将context 作为第一个参数,并且不断的透传下去,这样的效果就是整个项目中到处都是context。但是仅仅使用透传将context 传递下去并不能做到,取消信号,超时等功能,context 本身的机制采用的是通知机制,简单的透传并不能起作用。
1 |
|
在上面的例子中,虽然context 进行了透传,但是没有正确的使用就不会起作用
使用
父context
context 本身就是一个interface{}
1 |
|
Deadline
简单理解为,过期时间是什么时候,通过这个方法,就可以获取停止的时间
Done
done 本身是一个阻塞的channel
当已经完成了context 阻塞就会消失
Err
如果Done尚未关闭,Err返回nil。
如果Done关闭,Err将返回一个非零错误,解释原因:
如果上下文被取消,则取消
如果上下文的截止日期已过,则为DeadlineExceeded。
在Err返回非零错误之后,对Err的连续调用将返回相同的错误。
Value
可以用于取值,可以 简单的理解
这个Value 可以用于取最下层的context 的值,如果被设置好了
创建context 的方式有两个函数
context.TODO
和context.Backgroud
1 |
|
可以发现使用Backgroud 创建的channel 是最顶层的channel ,不会被取消,没有值,也不会deadline。通过观察源码发现,所谓的channel 其实就是一个空context
1 |
|
可以发现,todo 和backgroud 创建的context 都是空context
上面的两种方式是创建根context
,不具备任何功能,所以在此称之为父context
子context
可以使用下面的方法创建具有对应功能的context
1 |
|
这些就具有对应的功能
并且使用这些方法,创建的context 可以认为是父context 的一个衍生,当创建的数量够多了,就形成了一个context 树
$图片取自【Go语言】小白也能看懂的context包详解:从入门到精通 - 个人文章 - SegmentFault 思否$
并且,我们可以依赖于我们创建的子节点创建新的context
WithValue
携带数据
我们日常在业务开发中都希望能有一个trace_id
能串联所有的日志,这就需要我们打印日志时能够获取到这个trace_id
,在python
中我们可以用gevent.local
来传递,在java
中我们可以用ThreadLocal
来传递,在Go
语言中我们就可以使用Context
来传递,通过使用WithValue
来创建一个携带trace_id
的context
,然后不断透传下去,打印日志时输出即可
1 |
|
可以查看下面的这个例子
1 |
|
我们基于context.Background
创建一个携带trace_id
的ctx
,然后通过context
树一起传递,从中派生的任何context
都会获取此值,我们最后打印日志的时候就可以从ctx
中取值输出到日志中。目前一些RPC
框架都是支持了Context
,所以trace_id
的向下传递就更方便了。
使用WithValue
的注意事项
- 不建议使用
context
值传递关键参数,关键参数应该显示的声明出来,不应该隐式处理,context
中最好是携带签名、trace_id
这类值。 - 因为携带
value
也是key
、value
的形式,为了避免context
因多个包同时使用context
而带来冲突,key
建议采用内置类型。 - 上面的例子我们获取
trace_id
是直接从当前ctx
获取的,实际我们也可以获取父context
中的value
,在获取键值对是,我们先从当前context
中查找,没有找到会在从父context
中查找该键对应的值直到在某个父context
中返回nil
或者查找到对应的值。 context
传递的数据中key
、value
都是interface
类型,这种类型编译期无法确定类型,所以不是很安全,所以在类型断言时别忘了保证程序的健壮性。
超时控制
通常健壮的程序都是要设置超时时间的,避免因为服务端长时间响应消耗资源,所以一些web
框架或rpc
框架都会采用withTimeout
或者withDeadline
来做超时控制,当一次请求到达我们设置的超时时间,就会及时取消,不在往下执行。withTimeout
和withDeadline
作用是一样的,就是传递的时间参数不同而已,他们都会通过传入的时间来自动取消Context
,这里要注意的是他们都会返回一个cancelFunc
方法,通过调用这个方法可以达到提前进行取消,不过在使用的过程还是建议在自动取消后也调用cancelFunc
去停止定时减少不必要的资源浪费。
withTimeout
、WithDeadline
不同在于WithTimeout
将持续时间作为参数输入而不是时间对象,这两个方法使用哪个都是一样的,看业务场景和个人习惯了,因为本质withTimout
内部也是调用的WithDeadline
。
现在我们就举个例子来试用一下超时控制
当超过时间就终止下面的执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26func newContextWithTimeOutForOver() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), 4*time.Second)
}
func dealForOver(ctx context.Context) {
for i := 0; i < 10; i++ {
time.Sleep(1 * time.Second)
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Println("deal time is:", i)
}
}
}
func handlerForOver() {
ctx, cancelFunc := newContextWithTimeOutForOver()
defer cancelFunc()
dealForOver(ctx)
}
func TestForOver() {
handlerForOver()
}没有超过时间,但是手动终止
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28func newContextWithTimeOutForNoOver() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), 4*time.Second)
}
func dealForNoOver(ctx context.Context, cancelFunc context.CancelFunc) {
for i := 0; i < 10; i++ {
time.Sleep(1 * time.Second)
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Println("deal time is:", i+1)
cancelFunc()
}
}
}
func handlerForNoOver() {
ctx, cancelFunc := newContextWithTimeOutForNoOver()
defer cancelFunc()
dealForNoOver(ctx, cancelFunc)
}
func TestForNoOver() {
handlerForNoOver()
}
注意
总结
context
包主要提供了两种方式创建context
:
context.Backgroud()
context.TODO()
这两个函数其实只是互为别名,没有差别,官方给的定义是:
context.Background
是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来。context.TODO
应该只在不确定应该使用哪种上下文时使用;所以在大多数情况下,我们都使用
context.Background
作为起始的上下文向下传递。上面的两种方式是创建根
context
,不具备任何功能
使用起来还是比较容易的,既可以超时自动取消,又可以手动控制取消。这里大家要记的一个坑,就是我们往从请求入口透传的调用链路中的context
是携带超时时间的,如果我们想在其中单独开一个goroutine去处理其他的事情并且不会随着请求结束后而被取消的话,那么传递的context
要基于context.Background
或者context.TODO
重新衍生一个传递,否决就会和预期不符合了,可以参考context使用不当引发的一个bug (qq.com)
WithCancel取消控制
手动关闭
1 |
|
自定义context
因为Context
本质是一个接口,所以我们可以通过实现Context
达到自定义Context
的目的,一般在实现Web
框架或RPC
框架往往采用这种形式,比如gin
框架的Context
就是自己有封装了一层
参考
【Go语言】小白也能看懂的context包详解:从入门到精通 - 个人文章 - SegmentFault 思否
Golang深入浅出之-Go语言上下文(context)包:处理取消与超时-腾讯云开发者社区-腾讯云 (tencent.com)