以下文章来源于大愚Talk ,作者大愚Talk
我们为什么需要 Context 的呢?我们来看看看一个 HTTP 请求的处理:
请求示意
例子大概意思是说,有一个获取订单详情的请求,会单独起一个 goroutine 去处理该请求。在该请求内部又有三个分支 goroutine 分别处理订单详情、推荐商品、物流信息;每个分支可能又需要单独调用DB、Redis等存储组件。那么面对这个场景我们需要哪些额外的事情呢?
三个分支 goroutine 可能是对应的三个不同服务,我们想要携带一些基础信息过去,比如:LogID、UserID、IP等;每个分支我们需要设置过期时间,如果某个超时不影响整个流程;如果主 goroutine 发生错误,取消了请求,对应的三个分支应该也都取消,避免资源浪费;简单归纳就是传值、同步信号(取消、超时)。
看到这里可能有人要叫了,完全可以用 channel 来搞啊!那么我们看看 channel 是否可以满足。想一个问题,如果是微服务架构,channel 怎么实现跨进程的边界呢?另外一个问题,就算不跨进程,如果嵌套很多个分支,想一想这个消息传递的复杂度。
如果是你,要实现上面的这个需求,你会怎么做?
Context 出场幸好,我们不用自己每次写代码都要去实现这个很基础的能力。Golang 为我们准备好了一切,就是 context.Context 这个包,这个包的源代码非常简单,源码部分本文会略过,下期单独一篇文章来讲,本篇我们重点谈正确的使用。
Context 的结构非常简单,它是一个接口。
// Context 提供跨越API的截止时间获取,取消信号,以及请求范围值的功能。// 它的这些方案在多个 goroutine 中使用是安全的type Context interface { // 如果设置了截止时间,这个方法ok会是true,并返回设置的截止时间 Deadline() (deadline time.Time, ok bool) // 如果 Context 超时或者主动取消返回一个关闭的channel,如果返回的是nil,表示这个 // context 永远不会关闭,比如:Background() Done() <-chan struct{} // 返回发生的错误 Err() error // 它的作用就是传值 Value(key interface{}) interface{}}写到这里,我们打住想一想,如果你来实现这样一个能力的 package,你抽象的接口是否也是具备这样四个能力?
获取截止时间获取信号获取信号产生的对应错误信息传值专用net/ 这个包是怎么使用的。
func main() { req, _ := (肯定无法访问到)。执行时我们看到控制台做如下输出:
2020/xx/xx xx:xx:xx request Err Get 我们继续做实验,将上面的代码稍作修改。
func main() { req, _ := () 是能够拿到的,一旦获取到就会进行取消。上面的代码,控制台会输出:
2020/xx/xx xx:xx:xx request Err Get 注意两次控制台输出的错误信息是不一样的。
context deadline exceeded 表示执行超时被取消了context canceled 表示主动取消net/ 的部分,其它的直接忽略,源码路径如下;
net/)
// req 就是我们上面传进来的 req,它有个 context 字段func (t *Transport) roundTrip(req *Request) (*Response, error) { t.nextProtoOnce.Do(t.onceSetNextProtoDefaults) ctx := req.Context() // 获取了 context trace := 的信号。
这里隐藏了一个细节,那就是如果按照上面的逻辑只能处理到发起请求前的超时,但是如果请求已经被发出去了,等待这段时间的超时该如何控制呢?感兴趣的小伙伴可以去看源码的这里:
net/)
其实就是在内部等待返回的时候不断的检查 ctx.Done() 信号,如果发现了就立即返回。
好了,官方的技巧我们已经学完了,现在轮到我们把开头的例子写个代码来实现下。
多个 goroutine 控制超时及传值由于服务内部不方便模拟,我们简化成函数调用,假设图中所有的逻辑都可以并发调用。现在我们的要求是:
整个函数的超时时间为1s;需要从最外层传递 LogID/UserID/IP 信息到其它函数;获取订单接口超时为 500ms,由于 DB/Redis 是其内部支持的,这里不进行模拟;获取推荐超时是 400ms;获取物流超时是 700ms。为了清晰,我这里所有接口都返回一个字符串,实际中会根据需要返回不同的结果;请求参数也都只使用了 context。代码如下:
type key intconst ( userIP = iota userID logID)type Result struct { order string logistics string recommend string}// timeout: 1s// 入口函数func api() (result *Result, err error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) defer cancel() // 设置值 ctx = context.WithValue(ctx, userIP, "127.0.0.1") ctx = context.WithValue(ctx, userID, 666888) ctx = context.WithValue(ctx, logID, "123456") result = &Result{} // 业务逻辑处理放到协程中 go func() { result.order, err = getOrderDetail(ctx) }() go func() { result.logistics, err = getLogisticsDetail(ctx) }() go func() { result.recommend, err = getRecommend(ctx) }() for { select { case <-ctx.Done(): return result, ctx.Err() // 取消或者超时,把现有已经拿到的结果返回 default: } // 有错误直接返回 if err != nil { return result, err } // 全部处理完成,直接返回 if result.order != "" && result.logistics != "" && result.recommend != "" { return result, nil } }}// timeout: 500msfunc getOrderDetail(ctx context.Context) (string, error) { ctx, cancel := context.WithTimeout(ctx, time.Millisecond*500) defer cancel() // 模拟超时 time.Sleep(time.Millisecond * 700) // 获取 user id uip := ctx.Value(userIP).(string) fmt.Println("userIP", uip) return handleTimeout(ctx, func() string { return "order" })}// timeout: 700msfunc getLogisticsDetail(ctx context.Context) (string, error) { ctx, cancel := context.WithTimeout(ctx, time.Millisecond*700) defer cancel() // 获取 user id uid := ctx.Value(userID).(int) fmt.Println("userID", uid) return handleTimeout(ctx, func() string { return "logistics" })}// timeout: 400msfunc getRecommend(ctx context.Context) (string, error) { ctx, cancel := context.WithTimeout(ctx, time.Millisecond*400) defer cancel() // 获取 log id lid := ctx.Value(logID).(string) fmt.Println("logID", lid) return handleTimeout(ctx, func() string { return "recommend" })}// 超时的统一处理代码func handleTimeout(ctx context.Context, f func() string) (string, error) { // 请求之前先去检查下是否超时 select { case <-ctx.Done(): return "", ctx.Err() default: } str := make(chan string) go func() { // 业务逻辑 str <- f() }() select { case <-ctx.Done(): return "", ctx.Err() case ret := <-str: return ret, nil }}不知道你是否看明白了整个使用,我们这个例子看起来很复杂,实际上与我给你介绍的 net/ 的控制超时代码不需要我们写,而且我们这里一次性把三个调用的整合到了一起。
还有一点说明一下,对于 select,如果没有写 defalut 分支,是不需要放在 for 循环中的,因为它本身就会阻塞(网络上有很多例子放在for循环中)。
参考资料
[1] Package context[2] Go Concurrency Patterns: Context