在微服务的架构体系中,经常需要调用外部的接口,而外部接口一般是不可控制的,它随时可能会down
掉,也可能会由于网络问题或者外部接口内部问题导致超时,我们作为调用方,不可能永久的等待下去,所以调用方一般会设置一个超时时间。
在golang
中一般会使用chan
和select
以及time.After
控制超时,属于一种惯用写法了,如下
package main
import (
"net/http"
"time"
)
func readDB() string {
time.Sleep(200 * time.Millisecond)
return "ok"
}
func home(w http.ResponseWriter, req *http.Request) {
var resp string
done := make(chan struct{}, 1) // 使用chan,完成协调通信
go func() { // 开启协程去完成业务逻辑
resp = readDB()
done <- struct{}{} // 业务逻辑完成后,往通道中写入消息
}()
select { // 监听相关chan,如果300ms后,业务逻辑都没有完成,则会进入超时的case
case <-done:
case <-time.After(300 * time.Millisecond):
resp = "timeout"
}
w.Write([]byte(resp))
}
func main() {
http.HandleFunc("/", home)
http.ListenAndServe("127.0.0.1:8080", nil)
}
启动后,访问结果如下,因为读取DB
耗时200ms
,没有超过300ms
,所以没有超时,返回了ok
当把DB
中的耗时改为400ms
后,再次访问,则返回的是超时了
上面的实现实际还不够优雅,因为是手动写的chan
去控制,工作中更常用的是context
,可以应对子协程非常多,且树形结构复杂的协程树退出。
package main
import (
"context"
"fmt"
"time"
)
// 场景:设定一个超时时间,若是在指定超时时间后没有返回结果,则重试
func main() {
// 经过context的WithTimeout设置一个有效时间为800毫秒的context
// 该context会在耗尽800毫秒后或者方法执行完成后结束,结束的时候会向通道ctx.Done发送信号
start := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Millisecond*800))
defer func() {
// 注意,这里要记得调用cancel(),否则即便提早执行完了,还要傻傻等到WithTimeout设置的时间(800毫秒)后context才会被释放
cancel()
fmt.Println(time.Since(start))
}()
// 注意ctx应该作为子协程的第一个参数
go func(ctx context.Context) {
// 发送HTTP请求
fmt.Println("处理请求!")
time.Sleep(300 * time.Millisecond) // 假设业务逻辑耗时300ms
fmt.Println("请求处理完毕!")
}(ctx)
// 主要看WithTimeout设置的时间和下面time.After设置的时间谁先到就走谁,或者在time.After设置的时间到达前主动调用cancel,则ctx.Done产生的chan会关闭
// 关闭的chan是默认可读的,因此会走ctx.Done的case
select {
case <-ctx.Done(): // 800ms后,通道关闭,ctx.Done()通道可读
fmt.Println("call successfully!!!")
return
// 这里已经设置了context的有效时间,为何还要加上这个time.After呢
// 这是由于该方法内的context是本身声明的,能够手动设置对应的超时时间,可是在大多数场景,这里的ctx是从上游一直传递过来的,
// 对于上游传递过来的context还剩多少时间,咱们是不知道的,因此这时候经过time.After设置一个本身预期的超时时间就颇有必要了
case <-time.After(time.Duration(time.Millisecond * 700)):
fmt.Println("timeout!!!")
return
}
}