昨晚和朋友一起尝试解决Golang 调用的API 处理时间过长,客户端等不及的问题。

场景

1
2
3
4
5
6
7
/*
客户端 ---->  service A  ---> service B + cache
  |  <----     |								|
  |            |   --------->   | 
  | <--------- |   <---------   |             
*/ 
  

我们的服务A,要调用服务B的API, 服务B需要时间处理,来不及立刻返回结果给A

现在客户端调用A,希望A能返回一个信号,说任务在处理中了。过段时间再来去检验结果。 服务A过几分钟后再调用服务B,服务B会把上次的查询结果从Cache里面取出来,返回给服务A

解法

1. go func

在我们调用服务B的函数前面加一个“go”,说明不用等结果,先返回给客户端

2. 使用channel

(TBD)

3. 使用超时处理

大家有兴趣可以阅读Golang http请求的代码,如果只看超时/取消的原理,其实不难,模型其实都是一样的,通过select以及cancel channel来提前返回到底是超时还是正常的http事务,几乎所有可能超时的链路都使用了这个模型,比较有go的味道。

不过还是推荐使用context来取消/超时,不然channel到处乱飞,可维护性比较差。在我们自己的业务代码中,也可以通过这种方式来实现超时。

超时通用的代码模型如下,业务代码放到fn里:

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
type fn func(ctx context.Context) result

type result struct {
	Val interface{}
	Err error
}

func doWithTimeout(ctx context.Context, fn fn) result {
	ch := make(chan result)
	go func(ctx context.Context, ch chan<- result) {
		ch <- fn(ctx)
	}(ctx, ch)

	select {
	case <-ctx.Done(): // timeout
		go func() { <-ch }() // wait ch return...
		return result{Err: ctx.Err()}
	case res := <-ch: // normal case
		return res
	}
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
    defer cancel()
    result := doWithTimeout(ctx, func(ctx context.Context) result {
        // replace with your own logic!
        req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.google.com/", nil)
        resp, err := http.DefaultClient.Do(req)
        return result{
            Val: resp,
            Err: err,
        }
    })
    switch {
    case ctx.Err() == context.DeadlineExceeded:
        // handle timeout
    case result.Err != nil:
        // handle logic error
    default:
        // do with result.Val
    }
}

将具体的逻辑放到了一个独立的协程中,然后select等待,需要注意的是如果已经存在上下了,没有特别的理由,就基于该context追加超时,而不是像上面那样使用context.Background()

最后还需要说明一下,超时虽然能够检测到,但是取消并不是那么容易,比如网络的不确定性,接收端很可能已经处理请求了,所以具体取消与否还是依赖于服务器存储的状态。

详细请参考:https://www.xiaolongtongxue.com/articles/2021/how-does-go-handle-http-request-timeout-and-cancel