一个常见的场景:有一个接口请求需要较长的时间(如5秒),那么,用户很可能等不及,直接就放弃了请求。
而这个接口的任务,如果在用户放弃请求后依然继续执行,那么就是浪费服务器的资源了。
所以,我们需要在得知请求中断后,主动结束耗时的任务。
这里面其实都是用到go 提供的一个上下文库context
。
然而这个问题的核心是如何得知http的请求是否中断。这个问题在网上居然没什么资料。。。
示例演示
首先,我模拟了两个任务:任务taskFunc1
需要执行1秒,任务taskFunc2
需要执行5秒:
// 该方法模拟进行任务,需要1秒钟
func taskFunc1(ctx context.Context, end chan string) {
// 监听任务执行情况的协程
result := make(chan string)
// 启动协程执行任务
go func() {
time.Sleep(1 * time.Second)
result <- "success" // 5秒后完成任务,将结果写入信道
}()
// 监听 context的结束信号和任务结束的信号,哪个先到就先执行哪个
select {
case <-ctx.Done():
end <- "taskFunc1失败"
case <-result:
end <- "taskFunc1成功"
}
}
// 该方法模拟进行任务,需要5秒钟,和上面是一样的
func taskFunc2(ctx context.Context, end chan string) {
result := make(chan string)
go func() {
time.Sleep(5 * time.Second)
result <- "success"
}()
select {
case <-ctx.Done():
end <- "taskFunc2失败"
case <-result:
end <- "taskFunc2成功"
}
}
他们内容都是一样的,都有两个参数:
ctx context.Context
:这个参数就是上下文,对于http请求,当然http请求
就应该作为这个上文
,而任务
就是这下文
,上文如果主动中断了,那么下文也应该跟着中断end chan string
:一个信道,为了通知主线程(如main方法)任务是否执行完成。
接下来实现一个接口:
// 1.一个测试接口
func Test1(c *gin.Context) {
ctx := c.Request.Context() // 获取http请求的上下文对象
// 主线程判断两个协程是否运行结束的信道
end1 := make(chan string)
end2 := make(chan string)
// 启动两个协程分别完成任务
go taskFunc1(ctx, end1)
go taskFunc2(ctx, end2)
// 监听两个协程是否结束
err2, err1 := <-end2, <-end1
fmt.Println(err1, err2)
}
当我们正常发起http请求后,后台打印的内容应该如下:
两个任务都成功执行,整个请求的时间大约是5秒,因为任务taskFunc2
需要5秒钟。
然而,我用postman设置请求超时时间为3秒:
然后发起请求:
可以看到,taskFunc2
失败了,整个请求时间是大约3秒,看到了吧,http请求中断后,我们的任务也主动结束了,而不是等taskFunc2
执行完,这就是我想要的效果。
gin.Context
其实上面例子就为了证明,gin.Context
是一个上文,能够告知下文当前的状态。之前我还不理解为什么他叫Context
,现在知道了,其实一次http请求就被gin封装成了一个Context。
gin.Context和golang的context
包,本质上是一个意思。
而获取这个上下文的方法就是:gin.Context.Request.Context()
适用场景
其实在此之前,我连context
的存在都不知道,虽然用了gin很久,但是始终不理解它gin.Context
的含义。
而这次去了解这玩意,是因为我需要用到mongodb和redis,这两个数据库的驱动分别是mongo-driver
和go-redis
,我就发现这两个驱动设计的api,都要传入参数context.Context
。
我就很纳闷呀,这个参数有什么用,还非得我传,每次还得给他来一下context.TODO()
。所以,这两天就学习了一下这个context
包。
于是就想到了gin的Context原来是这么来的,那么它一定也具有context
的作用吧(然而百度搜来搜去都是一些原理和基础的介绍,很少有介绍这种细节的)。于是就有了上面的演示
于是现在也就知道了mongodb和redis要求传入这个参数的意义了:就是为了让我们能够主动结束它的任务,而不至于浪费资源继续请求数据,毕竟数据库资源是宝贵的。
当然,我们有必要每次都将gin的Context传入这些api吗?其实没有必要,因为大多数对数据库资源的请求也就是那么几十毫秒,为了节省这点资源,我们的代码需要多写很多,个人认为不需要做这种意义不大的事。
所以,我们还是针对一些特定的场景才使用,比如某些任务需要很长的时间等等。
所以,我们大多数时候,直接给api传入context.TODO()
即可。
那么,为什么gorm对数据库的操作又不需要这个参数呢?还没了解过,以后再说
但是注意:在中间件中,无法使用gin.Context.Request.Context().Done()
方法去检验请求是否终止,在中间件中,始终不会有返回值。