在服务器开发领域,有很多未知的原因可能导致用户的请求失败。比如:你的服务器并发请求了多个下游服务,并在等待它们返回所有结果。但是很不幸,其中一个服务返回了错误,此时其它所有其它请求我们也就没有必要再等待下去了,而是直接返回并告诉用户本次请求失败。
上面的场景在服务器开发领域非常常见,处理起来也比较困难。无论你是写 C++ 还是 Java,或是 Nodejs,都不那么容易处理。
多说不益,我们使用一个简单的服务器模拟请求下游,并人为取消请求。
1. 请求模拟
目标:假设我们的服务同时发起三次 HTTP GET 请求下游,得到结果,并打印到屏幕。
1.1 模拟的 Get 请求函数
下面是我们自己编写的一个模拟发送 HTTP GET 请求的函数。当然它并不会真的请求某个真实的下游。这个函数可以模拟网络拥塞的场景,比如长时间不返回结果。为了能让你看到效果,我特意将请求耗时随机设置在 [2,7] 秒之间。
关于 time.After 函数就不用我多说了,前面已经讲过了,当然你也可以自行查阅文档。
func Get() string {
duration := rand.Intn(5) + 2
tick := time.After(time.Duration(duration) * time.Second)
select {
case <-tick:
return fmt.Sprintf("get page %d", duration)
}
}
1.2 模拟请求下游服务
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
func Get() string {
duration := rand.Intn(5) + 2
tick := time.After(time.Duration(duration) * time.Second)
select {
case <-tick:
return fmt.Sprintf("get page %d", duration)
}
}
func main() {
rand.Seed(time.Now().Unix())
var wg sync.WaitGroup
wg.Add(3)
go func() {
fmt.Println(Get())
wg.Done()
}()
go func() {
fmt.Println(Get())
wg.Done()
}()
go func() {
fmt.Println(Get())
wg.Done()
}()
wg.Wait()
}
下面是运行效果。
图1 并发 Get 请求
2. cancel 请求
在上一篇《火箭发射游戏》里,你已经学会了使用 channel 来取消火箭发射。其实在这里,你仍然可以借鉴这个思路。
具体的,我们可以通过创建一个 channel 来控制。修改的代码如下:
package main
import (
"fmt"
"math/rand"
"os"
"sync"
"time"
)
func Get(done <-chan struct{}) string {
duration := rand.Intn(5) + 2
tick := time.After(time.Duration(duration) * time.Second)
// 使用 select 来监听 channel
select {
case <-tick:
return fmt.Sprintf("get page %d", duration)
case <-done:
return fmt.Sprintf("cancel %d", duration)
}
}
func main() {
rand.Seed(time.Now().Unix())
// 创建一个 channel
done := make(chan struct{})
var wg sync.WaitGroup
go func() {
os.Stdin.Read(make([]byte, 1))
// 因为我们发起了 3 次 Get 请求,所以要发送 3 次数据到 done channel
for n := 0; n < 3; n++ {
done <- struct{}{}
}
}()
wg.Add(3)
go func() {
fmt.Println(Get(done))
wg.Done()
}()
go func() {
fmt.Println(Get(done))
wg.Done()
}()
go func() {
fmt.Println(Get(done))
wg.Done()
}()
wg.Wait()
}
这一次,我们再次运行,并在请求结束前,按下你的 ENTER 键来结束请求。
图2 cancel 请求
在图 2 中,第一次我没有打断请求,程序自然结束。第二次,程序一启动我就按下 ENTER 打断了请求。第三次,程序第一个请求结束后我按下 ENTER 键,打断了后面的两个 GET 请求。
那么,我们的任务就到此为止了吗?显然不会。上面的实现太不优雅了,假设我们有 100 个并发请求出去了?难道就得把循环向 channel 发送 100 次数据吗?
有没有一种方向,可以广播的通知所有的 goroutine? 答案是有的。我们利用 channel 的另一个特性:如果从一个已经关闭的 channel 读取数据,程序会立即返回而不阻塞。
这样的就简单了,我们只要稍微修改一下那个看起来不太舒服的 for 循环就行:
// 不要使用这种方式了
for n := 0; n < 3; n++ {
done <- struct{}{}
}
// 改成下面这样
close(done)
稍作修改,你的程序就能达到和图 2 一样的效果。
3. 总结
- 掌握 channel 的关闭后的特性
- 掌握取消并发 goroutine 的方法
在 Golang 里,取消请求太常见了,以致于 Golang 为其实现了一些标准库来完成这些事情。下一讲,我们来看看它到底是什么。