在并发编程中,错误处理可能难以正确运行。有时候,我们花了很多时间思考我们的各种流程将如何共享信息和协调,却忘记考虑如何优雅地处理错误。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)就本书提出修改意见。