前言
有时我们要通过第三方服务获取数据,它可以是外部提供的 API,也可以是微服务的接口等等,总之,它们有相同的问题:“获取数据可能需要大量时间”。如果在代码中同步地获取这些数据,程序就会花时间等待这些服务响应,而这些等待会严重影响程序的运行效率,而且一旦这些服务崩溃,我们的程序就会陷入无休止的等待中,那么如何解决这个问题呢?可以使用 Go 的 context 包。
问题
我们用这个函数来替代那些第三方服务。我们直接使用 time.Sleep()
函数来模拟一个耗时过程,在现实场景中,它可能是在执行一个非常复杂的 SQL 查询,也可以是调用一个人工智能服务接口。当然,这个耗时是不确定的,甚至有可能是无穷大(卡死)。
func fetchThirdPartyStuffWhichCanBeSlow() (int, error) {
time.Sleep(time.Millisecond * 500)
return 64, nil
}
如果我们不做任何处理,直接调用这个函数,就像这样:
func foo() {
// some code here ...
val, err := fetchThirdPartyStuffWhichCanBeSlow()
if err != nil {
log.Fatal(err)
}
// some code here ...
}
上面的代码如果用在一些只执行一次的脚本、工具中,并不会带来严重后果,无非多等一下就好了,即使有问题也可以关掉程序检查一下第三方服务。但是如果上面的代码用在一个承载大流量的 web 服务中,程序在执行完耗时代码后还要继续执行一些重要的业务功能,那么这样直接调用而不加考虑的代码很可能是致命的。一旦第三方服务出现问题,程序没有任何机制检查和处理,而是直接陷入无休止的等待。这显然是不合理的。
解决方案
要解决上述的问题,比较常见的思路是引入一个主动停止耗时服务的功能,这样如果耗时函数花了太多时间执行,程序就可以感知到,并主动干预。
在后文中,我们假设我们要使用用户的 ID 访问用户的数据,且调用三方服务的代码被单独封装为 fetchUserData()
。
使用 Channel
如果不使用本文要介绍的 Context,传统的思路是使用 Channel + Select 来处理:
type Response struct {
value int
err error
}
func fetchUserData(userID int) (int, error) {
stop := make(chan bool)
respch := make(chan Response)
go func() {
val, err := fetchThirdPartyStuffWhichCanBeSlow()
respch <- Response{
value: val,
err: err,
}
}()
go func() {
time.Sleep(time.Millisecond * 200)
stop <- true
}()
for {
select {
case <-stop:
return 0, fmt.Errorf("fetching data from third party took to long")
case resp := <-respch:
return resp.value, resp.err
}
}
}
这里我们使用 stop 这个 Channel 来发送停止信号,在程序执行超过指定时间时关掉终止等待并报错,而 respch 用来接受返回值。在程序的最后,使用 select 来接受 Channel 的信号,当检测到超时或执行完成时返回结果。
使用 Context
Context 的基础用法其实就是对上述代码的优化:
func foo() {
// some code here ...
ctx := context.Background()
val, err := fetchUserData(ctx, userID)
// some code here ...
}
type Response struct {
value int
err error
}
func fetchUserData(ctx context.Context, userID int) (int, error) {
ctx, cancel := context.WithTimeout(ctx, time.Millisecond*200)
defer cancel()
respch := make(chan Response)
go func() {
val, err := fetchThirdPartyStuffWhichCanBeSlow()
respch <- Response{
value: val,
err: err,
}
}()
for {
select {
case <-ctx.Done():
return 0, fmt.Errorf("fetching data from third party took to long")
case resp := <-respch:
return resp.value, resp.err
}
}
}
这里,我们新建了一个 context:ctx, cancel := context.WithTimeout(ctx, time.Millisecond*200)
这个 context 带超时检测,超时后它会自动发出 ctx.Done()
这个信号,我们只需要在最后监测它即可。
传递数据
除了直接使用超时机制外,我们也可以通过 Context 传递数据:
func foo() {
// some code here ...
ctx := context.WithValue(context.Background(), "foo", "bar")
val, err := fetchUserData(ctx, userID)
// some code here ...
}
func fetchUserData(ctx context.Context, userID int) (int, error) {
value := ctx.Value("foo")
fmt.Println(value)
// some code here ...
}
优势在哪儿
使用 Context 可以减少 Channel 的使用,尤其是调用层级非常深时,使用 Channel 来传递关闭信号非常复杂,而 Context 可以轻松地传递关闭信号。