您现在的位置是:网站首页> 编程资料编程资料
Go语言学习之context包的用法详解_Golang_
2023-05-26
418人已围观
简介 Go语言学习之context包的用法详解_Golang_
前言
日常 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中。
func Stream(ctx context.Context, out chan<- Value) error { for { // 处理数据 v, err := DoSomething(ctx) if err != nil { return err } // ctx.Done() 读取到数据,说明获取到了任务取消的信号 select { case <-ctx.Done(): return ctx.Err() // 否则将结果输出,继续计算 case out <- v: } } } Value() 也是一个比较常用的方法,用于在上下文中传递一些数据。使用 context.WithValue() 方法存入 key 和 value,通过 Value() 方法则可以根据 key 拿到 value。
func main() { ctx := context.Background() c := context.WithValue(ctx, "key", "value") v, ok := c.Value("key").(string) fmt.Println(v, ok) } emptyCtx
Context 接口并不需要我们自己去手动实现,一般我们都是直接使用 context 包中提供的 Background() 方法和 TODO() 方法,来获取最基础的 Context。
var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo } Background() 方法一般用在 main 函数,或者程序的初始化方法中;在我们不知道使用哪个 Context,或者上文没有传递 Context时,可以使用 TODO()。
Background() 和 TODO() 都是基于 emptyCtx 生成的,从名字可以看出来,emptyCtx 是一个空的Context,没有 deadline、不能被取消、没有键值对。
type emptyCtx int func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { return } func (*emptyCtx) Done() <-chan struct{} { return nil } func (*emptyCtx) Err() error { return nil } func (*emptyCtx) Value(key interface{}) interface{} { return nil } func (e *emptyCtx) String() string { switch e { case background: return "context.Background" case todo: return "context.TODO" } return "unknown empty Context" } 除了上面两个最基本的 Context 外,context 包中提供了功能更加丰富的 Context,包括 valueCtx、cancelCtx、timerCtx,下面我们就挨个来看下。
valueCtx
使用示例
我们一般使用 context.WithValue() 方法向 Context 存入键值对,然后通过 Value() 方法根据 key 得到 value,此种功能的实现就依赖 valueCtx。
func main() { ctx := context.Background() c := context.WithValue(ctx, "myKey", "myValue") v1 := c.Value("myKey") fmt.Println(v1.(string)) v2 := c.Value("hello") fmt.Println(v2) // nil } 类型定义
valueCtx 结构体中嵌套了 Context,使用 key 、value 来保存键值对:
type valueCtx struct { Context key, val interface{} } WithValue
context包 对外暴露了 WithValue 方法,基于一个 parent context 来创建一个 valueCtx。从下面的源码中可以看出,key 必须是可比较的!
func WithValue(parent Context, key, val interface{}) Context { if parent == nil { panic("cannot create context from nil parent") } if key == nil { panic("nil key") } if !reflectlite.TypeOf(key).Comparable() { panic("key is not comparable") } return &valueCtx{parent, key, val} } *valueCtx 实现了 Value(),可以根据 key 得到 value。这是一个向上递归寻找的过程,如果 key 不在当前 valueCtx 中,会继续向上找 parent Context,直到找到最顶层的 Context,一般最顶层的是 emptyCtx,而 emtpyCtx.Value() 返回 nil。
func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key) } cancelCtx
cancelCtx 是一个用于取消任务的 Context,任务通过监听 Context 是否被取消,来决定是否继续处理任务还是直接返回。
如下示例中,我们在 main 函数定义了一个 cancelCtx,并在 2s 后调用 cancel() 取消 Context,即我们希望 doSomething() 在 2s 内完成任务,否则就可以直接返回,不需要再继续计算浪费资源了。
doSomething() 方法内部,我们使用 select 监听任务是否完成,以及 Context 是否已经取消,哪个先到就执行哪个分支。方法模拟了一个 5s 的任务,main 函数等待时间是2s,因此没有完成任务;如果main函数等待时间改为10s,则任务完成并会返回结果。
这只是一层调用,真实情况下可能会有多级调用,比如 doSomething 可能又会调用其他任务,一旦 parent Context 取消,后续的所有任务都应该取消。
func doSomething(ctx context.Context) (interface{}, error) { res := make(chan interface{}) go func() { fmt.Println("do something") time.Sleep(time.Second * 5) res <- "done" }() select { case <-ctx.Done(): return nil, ctx.Err() case value := <-res: return value, nil } } func main() { ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(time.Second * 2) cancel() }() res, err := doSomething(ctx) fmt.Println(res, err) // nil , context canceled } 接下来就让我们来研究下,cancelCtx 是如何实现取消的吧
类型定义
- canceler 接口包含 cancel() 和 Done() 方法,*cancelCtx 和 *timerCtx 均实现了这个接口。
- closedchan 是一个被关闭的channel,可以用于后面 Done() 返回
- canceled 是一个 err,用于 Context 被取消的原因
type canceler interface { cancel(removeFromParent bool, err error) Done() <-chan struct{} } // closedchan is a reusable closed channel. var closedchan = make(chan struct{}) func init() { close(closedchan) } var Canceled = errors.New("context canceled") CancelFunc 是一个函数类型定义,是一个取消函数,有如下规范:
- CancelFunc 告诉一个任务停止工作
- CancelFunc 不会等待任务结束
- CancelFunc 支持并发调用
- 第一次调用后,后续的调用不会产生任何效果
type CancelFunc func()
&cancelCtxKey 是一个固定的key,用来返回 cancelCtx 自身
var cancelCtxKey int
cancelCtx
cancelCtx 是可以被取消的,它嵌套了 Context 接口,实现了 canceler 接口。cancelCtx 使用 children 字段保存同样实现 canceler 接口的子节点,当 cancelCtx 被取消时,所有的子节点也会取消。
type cancelCtx struct { Context mu sync.Mutex // 保护如下字段,保证线程安全
相关内容
- go语言算法题解二叉树的最小深度_Golang_
- Go语言读写锁RWMutex的源码分析_Golang_
- Golang Mutex互斥锁源码分析_Golang_
- 初识Golang Mutex互斥锁的使用_Golang_
- golang 实现 pdf 转高清晰度 jpeg的处理方法_Golang_
- Golang交叉编译之跨平台编译使用详解_Golang_
- go 对象池化组件 bytebufferpool使用详解_Golang_
- Go项目实现优雅关机与平滑重启功能_Golang_
- Go语言操作Excel利器之excelize类库详解_Golang_
- 一篇文章带你搞懂Go语言标准库Time_Golang_
点击排行
本栏推荐
