Go语言 context包源码学习
作者:互联网
前言
日常 Go 开发中,Context 包是用的最多的一个了,几乎所有函数的第一个参数都是 ctx,那么我们为什么要传递 Context 呢,Context 又有哪些用法,底层实现是如何呢?相信你也一定会有探索的欲望,那么就跟着本篇文章,一起来学习吧!
需求一
开发中肯定会调用别的函数,比如 A 调用 B,在调用过程中经常会设置超时时间,比如超过2s 就不等待 B 的结果了,直接返回,那么我们需要怎么做呢?
// 睡眠5s,模拟长时间操作
func FuncB() (interface{}, error) {
time.Sleep(5 * time.Second)
return struct{}{}, nil
}
func FuncA() (interface{}, error) {
var res interface{}
var err error
ch := make(chan interface{})
// 调用FuncB(),将结果保存至 channel 中
go func() {
res, err = FuncB()
ch <- res
}()
// 设置一个2s的定时器
timer := time.NewTimer(2 * time.Second)
// 监测是定时器先结束,还是 FuncB 先返回结果
select {
// 超时,返回默认值
case <-timer.C:
return "default", err
// FuncB 先返回结果,关闭定时器,返回 FuncB 的结果
case r := <-ch:
if !timer.Stop() {
<-timer.C
}
return r, err
}
}
func main() {
res, err := FuncA()
fmt.Println(res, err)
}
上面我们的实现,可以实现超过等待时间后,A 不等待 B,但是 B 并没有感受到取消信号,如果 B 是个计算密度型的函数,我们也希望B 感知到取消信号,及时取消计算并返回,减少资源浪费。
另一种情况,如果存在多层调用,比如A 调用 B、C,B 调用 D、E,C调用 E、F,在超过 A 的超时时间后,我们希望取消信号能够一层层的传递下去,后续所有被调用到的函数都能感知到,及时返回。
需求二
在多层调用的时候,A->B->C->D,有些数据需要固定传输,比如 LogID,通过打印相同的 LogID,我们就能够追溯某一次调用,方便问题的排查。如果每次都需要传参的话,未免太麻烦了,我们可以使用 Context 来保存。通过设置一个固定的 Key,打印日志时从中取出 value 作为 LogID。
const LogKey = "LogKey"
// 模拟一个日志打印,每次从 Context 中取出 LogKey 对应的 Value 作为LogID
type Logger struct{}
func (logger *Logger) info(ctx context.Context, msg string) {
logId, ok := ctx.Value(LogKey).(string)
if !ok {
logId = uuid.New().String()
}
fmt.Println(logId + " " + msg)
}
var logger Logger
// 日志打印 并 调用 FuncB
func FuncA(ctx context.Context) {
logger.info(ctx, "FuncA")
FuncB(ctx)
}
func FuncB(ctx context.Context) {
logger.info(ctx, "FuncB")
}
// 获取初始化的,带有 LogID 的 Context,一般在程序入口做
func getLogCtx(ctx context.Context) context.Context {
logId, ok := ctx.Value(LogKey).(string)
if ok {
return ctx
}
logId = uuid.NewString()
return context.WithValue(ctx, LogKey, logId)
}
func main() {
ctx = getLogCtx(context.Background())
FuncA(ctx)
}
这利用到了本篇文章讲到的 valueCtx,继续往下看,一起来学习 valueCtx 是怎么实现的吧!
Context 接口
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Context 接口比较简单,定义了四个方法:
- Deadline() 方法返回两个值,deadline 表示 Context 将会在什么时间点取消,ok 表示是否设置了deadline。当 ok=false 时,表示没有设置deadline,那么此时 deadline 将会是个零值。多次调用这个方法返回同样的结果。
- Done() 返回一个只读的 channel,类型为 chan struct{},如果当前的 Context 不支持取消,Done 返回 nil。我们知道,如果一个 channel 中没有数据,读取数据会阻塞;而如果channel被关闭,则可以读取到数据,因此可以监听 Done 返回的 channel,来获取 Context 取消的信号。
- Err() 返回 Done 返回的 channel 被关闭的原因。当 channel 未被关闭时,Err() 返回 nil;channel 被关闭时则返回相应的值,比如 Canceled 、DeadlineExceeded。Err() 返回一个非 nil 值之后,后面再次调用会返回相同的值。
- Value() 返回 Context 保存的键值对中,key 对应的 value,如果 key 不存在则返回 nil。
Done() 是一个比较常用的方法,下面是一个比较经典的流式处理任务的示例:监听 ctx.Done() 是否被关闭来判断任务是否需要取消,需要取消则返回相应的原因;没有取消则将计算的结果写入到 out channel中。