在并发编程中,错误处理可能难以正确运行。有时候,我们花了很多时间思考我们的各种流程将如何共享信息和协调,却忘记考虑如何优雅地处理错误。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)
}
- 这个我们看到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)
}
- 这里我们创建一个包含*http.Response和goroutine循环迭代中可能出现的错误类型。
- 该行返回一个可读取的通道,以检索循环迭代的结果。
- 在这里,我们创建一个Result实例,并设置Error和Response字段。
- 这是我们将结果写入通道。
- 在这里,在我们的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可能产生错误,那么这些错误应该与你的结果类型紧密结合,并且通过相同的通信线路传递——就像常规的同步函数一样。