在并发编程中,错误处理可能难以正确运行。有时候,我们花了很多时间思考我们的各种流程将如何共享信息和协调,却忘记考虑如何优雅地处理错误。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)
}
  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可能产生错误,那么这些错误应该与你的结果类型紧密结合,并且通过相同的通信线路传递——就像常规的同步函数一样。

最后编辑: kuteng  文档更新时间: 2021-01-02 17:30   作者:kuteng