我对 Go 的错误处理有哪些不满,以及我是如何处理的

技术文章 来源:Linux中国 发布:2017-09-07 浏览:714

摘要:写 Go 的人往往对它的错误处理模式有一定的看法。按不同的语言经验,人们可能有不同的习惯处理方法。这就是为什么我决定要写这篇文章,尽管有点固执己见,但我认为听取我的经验是有用的。我想要讲的主要问题是,很难去强制执行良好的错误处理实践,错误经常没有堆栈追踪,并且错误处理本身太冗长。不过,我已经看到了一些潜在的解决方案,或许能帮助解决一些问题。

写 Go 的人往往对它的错误处理模式有一定的看法。按不同的语言经验,人们可能有不同的习惯处理方法。这就是为什么我决定要写这篇文章,尽管有点固执己见,但我认为听取我的经验是有用的。我想要讲的主要问题是,很难去强制执行良好的错误处理实践,错误经常没有堆栈追踪,并且错误处理本身太冗长。不过,我已经看到了一些潜在的解决方案,或许能帮助解决一些问题。

与其他语言的快速比较

在 Go 中,所有的错误都是值。因为这点,相当多的函数最后会返回一个 error, 看起来像这样:

  1. func (s *SomeStruct) Function() (string, error)

因此这导致调用代码通常会使用 if 语句来检查它们:

  1. bytes, err := someStruct.Function()
  2. if err != nil {
  3. // Process error
  4. }

另外一种方法,是在其他语言中,如 Java、C#、Javascript、Objective C、Python 等使用的 try-catch 模式。如下你可以看到与先前的 Go 示例类似的 Java 代码,声明 throws 而不是返回 error:

  1. public String function() throws Exception

它使用的是 try-catch 而不是 if err != nil:

  1. try {
  2. String result = someObject.function()
  3. // continue logic
  4. }
  5. catch (Exception e) {
  6. // process exception
  7. }

当然,还有其他的不同。例如,error 不会使你的程序崩溃,然而 Exception 会。还有其他的一些,在本篇中会专门提到这些。

实现集中式错误处理

退一步,让我们看看为什么要在一个集中的地方处理错误,以及如何做到。

大多数人或许会熟悉的一个例子是 web 服务 - 如果出现了一些未预料的的服务端错误,我们会生成一个 5xx 错误。在 Go 中,你或许会这么实现:

  1. func init() {
  2. http.HandleFunc("/users", viewUsers)
  3. http.HandleFunc("/companies", viewCompanies)
  4. }
  5. func viewUsers(w http.ResponseWriter, r *http.Request) {
  6. user // some code
  7. if err := userTemplate.Execute(w, user); err != nil {
  8. http.Error(w, err.Error(), 500)
  9. }
  10. }
  11. func viewCompanies(w http.ResponseWriter, r *http.Request) {
  12. companies = // some code
  13. if err := companiesTemplate.Execute(w, companies); err != nil {
  14. http.Error(w, err.Error(), 500)
  15. }
  16. }

这并不是一个好的解决方案,因为我们不得不重复地在所有的处理函数中处理错误。为了能更好地维护,最好能在一处地方处理错误。幸运的是,在 Go 语言的官方博客中,Andrew Gerrand 提供了一个替代方法,可以完美地实现。我们可以创建一个处理错误的 Type:

  1. type appHandler func(http.ResponseWriter, *http.Request) error
  2. func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  3. if err := fn(w, r); err != nil {
  4. http.Error(w, err.Error(), 500)
  5. }
  6. }

这可以作为一个封装器来修饰我们的处理函数:

  1. func init() {
  2. http.Handle("/users", appHandler(viewUsers))
  3. http.Handle("/companies", appHandler(viewCompanies))
  4. }

接着我们需要做的是修改处理函数的签名来使它们返回 errors。这个方法很好,因为我们做到了 DRY 原则,并且没有重复使用不必要的代码 - 现在我们可以在单独一个地方返回默认错误了。

错误上下文

在先前的例子中,我们可能会收到许多潜在的错误,它们中的任何一个都可能在调用堆栈的许多环节中生成。这时候事情就变得棘手了。

为了演示这点,我们可以扩展我们的处理函数。它可能看上去像这样,因为模板执行并不是唯一一处会发生错误的地方:

  1. func viewUsers(w http.ResponseWriter, r *http.Request) error {
  2. user, err := findUser(r.formValue("id"))
  3. if err != nil {
  4. return err;
  5. }
  6. return userTemplate.Execute(w, user);
  7. }

调用链可能会相当深,在整个过程中,各种错误可能在不同的地方实例化。Russ Cox的这篇文章解释了如何避免遇到太多这类问题的最佳实践:

“在 Go 中错误报告的部分约定是函数包含相关的上下文,包括正在尝试的操作(比如函数名和它的参数)。”

这个给出的例子是对 OS 包的一个调用:

  1. err := os.Remove("/tmp/nonexist")
  2. fmt.Println(err)

它会输出:

  1. remove /tmp/nonexist: no such file or directory

总结一下,执行后,输出的是被调用的函数、给定的参数、特定的出错信息。当在其他语言中创建一个 Exception 消息时,你也可以遵循这个实践。如果我们在 viewUsers 处理中坚持这点,那么几乎总是能明确错误的原因。

问题来自于那些不遵循这个最佳实践的人,并且你经常会在第三方的 Go 库中看到这些消息:

  1. Oh no I broke

这没什么帮助 - 你无法了解上下文,这使得调试很困难。更糟糕的是,当这些错误被忽略或返回时,这些错误会被备份到堆栈中,直到它们被处理为止:

  1. if err != nil {
  2. return err
  3. }

这意味着错误何时发生并没有被传递出来。

应该注意的是,所有这些错误都可以在 Exception 驱动的模型中发生 - 糟糕的错误信息、隐藏异常等。那么为什么我认为该模型更有用?

即便我们在处理一个糟糕的异常消息,我们仍然能够了解它发生在调用堆栈中什么地方。因为堆栈跟踪,这引发了一些我对 Go 不了解的部分 - 你知道 Go 的 panic 包含了堆栈追踪,但是 error 没有。我推测可能是 panic 会使你的程序崩溃,因此需要一个堆栈追踪,而处理错误并不会,因为它会假定你在它发生的地方做一些事。

所以让我们回到之前的例子 - 一个有糟糕错误信息的第三方库,它只是输出了调用链。你认为调试会更容易吗?

  1. panic: Oh no I broke
  2. [signal 原    文:What I Don’t Like About Error Handling in Go, and How to Work Around It
    译    文:Linux中国
    作    者:geekpi

免责声明:

  1. SDK.cn遵循行业规范,所有转载文章均征得作者同意授权并明确标注来源和链接。
  2. 我们十分尊重原创作者的付出,本站禁止二次转载如需转载请与原作者取得联系。
  3. 转载SDK.cn的原创文章需注明文章作者、链接和"来源:SDK.cn"并保留文章及标题完整性未经作者同意不得擅自修改。
  4. 作者投稿可能会经SDK.cn适当编辑修改或补充。