在并发编程中,错误处理可能难以正确运行。有时候,我们花了很多时间思考我们的各种流程将如何共享信息和协调,却忘记考虑如何优雅地处理错误。Go避开了流行的错误异常模型,Go认为错误处理非常重要,并且在开发程序时,我们应该像关注算法一样关注它。本着这种精神,让我们来看看在处理多个并发进程时我们如何做到这一点。
思考错误处理时最根本的问题是,“应该由谁负责处理错误?”在某些情况下,程序需要停止传递堆栈中的错误,并将它们处理掉,这样的操作应该何时执行呢?
在并发进程中,这样的问题变得愈发复杂。因为一个并发进程独立于其父进程或兄弟进程运行,所以可能很难推断出错误是如何产生的。
下面的就展示了这样的问题:
```
checkStatus := func(done <-chan interface{}, urls ...string, ) <-chan *http.Response {
responses := make(chan *http.Response)
go func() {
defer close(responses)
for _, url := range urls {
resp, err := http.Get(url)
if err != nil {
fmt.Println(err) //1
continue
}
select {
case <-done:
return
case responses <- resp:
}
}
}()
return responses
}
done := make(chan interface{})
defer close(done)
urls := []string{"https://www.baidu.com", "https://badhost"}
for response := range checkStatus(done, urls...) {
fmt.Printf("Response: %v\n", response.Status)
}
```
1. 这个我们看到goroutine尽其最大努力展示错误信号。但也仅仅是展示出来,它还能做什么? 它无法传回! 如果错误种类太多怎么办? 再请求一遍吗?
这会输出:
```
Response: 200 OK
Get https://badhost: dial tcp: lookup badhost on 127.0.1.1:53: no such host
```
我们看到代码中并没有给goroutine更多的选择以处理可能出现的错误。它不能简单的把这个错误不加任何处理的抛弃掉,所以当前唯一明智的做法是:它会打印错误并希望受到程序使用者的关注。别把你的goroutine像这样放到如此尴尬的处境之下。我建议你把程序的关注点分离:一般来说,你的并发进程应该把错误发送到你的程序的另一部分,这样程序状态的完整信息就被保留下来,并留出余地让使用者可以做出更明智的决定来处理它。我们对上面的例子做了一点点修改:
```
type Result struct { //1
Error error
Response *http.Response
}
checkStatus := func(done <-chan interface{}, urls ...string) <-chan Result { //2
results := make(chan Result)
go func() {
defer close(results)
for _, url := range urls {
var result Result
resp, err := http.Get(url)
result = Result{Error: err, Response: resp} //3
select {
case <-done:
return
case results <- result: //4
}
}
}()
return results
}
done := make(chan interface{})
defer close(done)
urls := []string{"https://www.baidu.com", "https://badhost"}
for result := range checkStatus(done, urls...) {
if result.Error != nil { //5
fmt.Printf("error: %v", result.Error)
continue
}
fmt.Printf("Response: %v\n", result.Response.Status)
}
```
1. 这里我们创建一个包含*http.Response和goroutine循环迭代中可能出现的错误类型。
2. 该行返回一个可读取的通道,以检索循环迭代的结果。
3. 在这里,我们创建一个Result实例,并设置Error和Response字段。
4. 这是我们将结果写入通道。
5. 在这里,在我们的main goroutine中,我们能够自行处理由checkStatus中出现的错误,并获取详细的响应信息。
这会输出:
```
Response: 200 OK
error: Get https://badhost: dial tcp: lookup badhost on 127.0.1.1:53: no such host
```
这里要注意的关键是我们如何将潜在的结果与潜在的错误结合起来。我们已经成功地将错误处理的担忧从生产者中分离出来。这是可取的,因为生成goroutine的goroutine(在这种情况下是我们的main goroutine)拥有更多关于正在运行的程序的上下文,并且可以做出关于如何处理错误的更明智的决定。
在前面的例子中,我们只是将错误写入stdio,但我们可以做其他事情。 让我们稍微修改我们的程序,以便在发生三个或更多错误时停止错误检查:
```
done := make(chan interface{})
defer close(done)
errCount := 0
urls := []string{"a", "https://www.baidu.com", "b", "c", "d"}
for result := range checkStatus(done, urls...) {
if result.Error != nil {
fmt.Printf("error: %v\n", result.Error)
errCount++
if errCount >= 3 {
fmt.Println("Too many errors, breaking!")
break
}
continue
}
fmt.Printf("Response: %v\n", result.Response.Status)
}
```
这会输出:
```
error: Get a: unsupported protocol scheme ""
Response: 200 OK
error: Get b: unsupported protocol scheme ""
error: Get c: unsupported protocol scheme ""
Too many errors, breaking!
```
你可以看到,因为错误是从checkStatus返回的而不是在goroutine内部处理的,所以错误处理遵循熟悉的Go规范。 这是个简单的例子,但不难想象在更大更复杂的的程序下是什么样子。这里的主要内容是,在构建从goroutines返回的价值时,应将错误视为一等公民。 如果你的goroutine可能产生错误,那么这些错误应该与你的结果类型紧密结合,并且通过相同的通信线路传递——就像常规的同步函数一样。
* * * * *
学识浅薄,错误在所难免。我是长风,欢迎来Golang中国的群(211938256)就本书提出修改意见。
- 前序
- 谁适合读这本书
- 章节导读
- 在线资源
- 第一章 并发编程介绍
- 摩尔定律,可伸缩网络和我们所处的困境
- 为什么并发编程如此困难
- 数据竞争
- 原子性
- 内存访问同步
- 死锁,活锁和锁的饥饿问题
- 死锁
- 活锁
- 饥饿
- 并发安全性
- 优雅的面对复杂性
- 第二章 代码建模:序列化交互处理
- 并发与并行
- 什么是CSP
- CSP在Go中的衍生物
- Go的并发哲学
- 第三章 Go的并发构建模块
- Goroutines
- sync包
- WaitGroup
- Mutex和RWMutex
- Cond
- Once
- Pool
- Channels
- select语句
- GOMAXPROCS
- 结论
- 第四章 Go的并发编程范式
- 访问范围约束
- fo-select循环
- 防止Goroutine泄漏
- or-channel
- 错误处理
- 管道
- 构建管道的最佳实践
- 便利的生成器
- 扇入扇出
- or-done-channel
- tee-channel
- bridge-channel
- 队列
- context包
- 小结
- 第五章 可伸缩并发设计
- 错误传递
- 超时和取消
- 心跳
- 请求并发复制处理
- 速率限制
- Goroutines异常行为修复
- 本章小结
- 第六章 Goroutines和Go运行时
- 任务调度