Go错误处理技巧

111

我刚开始接触Go语言。我的代码中开始出现很多这种情况:

   if err != nil {
      //handle err
   }

或者这个

  if err := rows.Scan(&some_column); err != nil {
      //handle err
  }

在Go语言中,有一些好的成语/策略/最佳实践来检查和处理错误吗?

编辑以澄清:我不是在抱怨或建议Go团队提出更好的方法。我想知道我是否做得对,或者是否错过了社区已经找到的某些技巧。谢谢大家。


4
没有,这是一个经常讨论且合理的话题。也有很多进化提案。团队的答案似乎是,在编写良好的代码中不应该成为问题。 - Denys Séguret
相关链接:https://dev59.com/rWUp5IYBdhLWcg3wRF6c?rq=1 - user2437417
请注意,这个相关的问题并不完全与这个相同。答案太具体了。 - Denys Séguret
这种烦恼也有其合理性:它使快速编写程序变得更加困难,但也使通过简单地重新抛出错误来创建错误变得更加困难。 - Denys Séguret
你可以在这个链接中找到Andrew Gerrand和Brad Fitzpatrick用Go语言写的HTTP/2客户端的初始代码。 - Supreet Sethi
11个回答

63

你的代码风格符合惯例,在我看来是最佳实践。当然也有人持不同意见,但我会辩称这种风格在 Golang标准库 中随处可见。换句话说,Go的作者们以这种方式编写错误处理。


12
“Go的作者们用这种方式编写错误处理。” 听起来不错。 - gmoore
“有些人肯定会持不同意见”:我不确定是否有人会说这不是当今最好的实践方法。有些人会要求语法糖或其他更改,但今天我认为任何严肃的编码人员都不会检查错误。 - Denys Séguret
@dystroy:我同意你的观点,我承认我的表述不够准确;-) - zzzz
2
处理错误的方式是语言设计的问题,这是一个极具争议的话题。幸运的是,有数十种语言可供选择。 - fuz
4
让我感到不爽的是每次函数调用都使用相同的模式。这使得代码在某些地方变得很杂乱,并且非常需要语法糖来简化代码而不失去任何信息,这本质上就是简洁性的定义(我认为这比冗长更优秀,但这可能是一个有争议的观点)。这个原则是正确的,但是语法在我看来还有很多改进空间。然而抱怨是被禁止的,所以我现在只能喝我的“果汁”;-) - Thomas
显示剩余2条评论

32
六个月后,Rob Pike撰写了一篇名为Errors are Values的博客文章。在其中,他认为你不需要像OP所展示的那样编程,并提到了标准库中使用不同模式的几个地方。
引用: 当然,涉及错误值的常见语句是测试它是否为nil,但还有无数其他事情可以用错误值做,应用其中一些其他事情可以使您的程序更好,消除了如果每个错误都通过机械的if语句检查而产生的大量样板文件。 ... 使用语言简化您的错误处理。 但请记住:无论您做什么,始终要检查您的错误!
这是一篇很好的读物。

这篇文章很棒,它基本介绍了一个对象可以处于失败状态,如果是这样,它将忽略你对它的所有操作并保持在失败状态。对我来说听起来像几乎单子。 - Waterlink
@Waterlink 你的陈述毫无意义。只要你稍微看一下,几乎所有有状态的东西都是单子模式。我认为将其与https://en.wikipedia.org/wiki/Null_Object_pattern进行比较更有用。 - user7610
@user7610,感谢您的反馈。我完全同意。 - Waterlink
3
派克(Pike)说:“但请记住:无论做什么,始终要检查错误!”——这听起来太80年代了。错误可能随时发生,停止加重程序员的负担,采用异常处理吧。 - Slawomir
太糟糕了!不仅没有一种单一标准的处理错误的方法,而且建议的解决方案还会在某人不熟悉特定处理方式时忽略掉错误。 - Fax
显示剩余2条评论

22

我同意jnml的答案,他们都是惯用代码。此外,我还要补充以下内容:

你的第一个例子:

if err != nil {
      //handle err
}

在处理多个返回值时,使用"更为惯用"。例如:

val, err := someFunc()
if err != nil {
      //handle err
}
//do stuff with val

如果函数只返回一个error,或者您有意忽略除error之外的返回值,那么您的第二个示例是一种很好的简写方式。例如,这在返回写入的字节数(有时是不必要的信息)和errorReaderWriter函数中有时会使用:

if _, err := f.Read(file); err != nil {
      //handle err
}
//do stuff with f

第二种形式被称为使用if初始化声明

因此,就最佳实践而言,据我所知(除了在需要创建新错误时使用"errors"包之外),您已经覆盖了Go中有关错误的几乎所有知识!

编辑:如果您真的无法离开异常,可以使用deferpanicrecover来模仿它们。


3
Go语言之神已经发布了Go 2中错误处理的“草案设计”。它旨在改变错误惯用语法。概述设计
他们希望得到用户的反馈!反馈维基 简而言之,它看起来像:
func f() error {
   handle err { fmt.Println(err); return err }
   check mayFail()
   check canFail()
}

更新:初稿设计受到了很多批评,因此我起草了考虑Go 2错误处理的要求,列出了可能的解决方案菜单。


3
我为简化错误处理和通过Go函数队列进行流水线处理而创建了一个库。
你可以在这里找到它: https://github.com/go-on/queue 它有紧凑和冗长的语法变体。以下是短语法的示例:
import "github.com/go-on/queue/q"

func SaveUser(w http.ResponseWriter, rq *http.Request) {
    u := &User{}
    err := q.Q(                      
        ioutil.ReadAll, rq.Body,  // read json (returns json and error)
    )(
        // q.V pipes the json from the previous function call
        json.Unmarshal, q.V, u,   // unmarshal json from above  (returns error)
    )(
        u.Validate,               // validate the user (returns error)
    )(
        u.Save,                   // save the user (returns error)
    )(
        ok, w,                    // send the "ok" message (returns no error)
    ).Run()

    if err != nil {
       switch err {
         case *json.SyntaxError:
           ...
       }
    }
}

请注意,这会带来一些性能开销,因为它使用了反射技术。
此外,这不是典型的 Go 代码,所以您可以在自己的项目中使用它,或者如果您的团队同意使用它。

5
仅仅因为你“能够”做这件事,并不代表这是一个好主意。这看起来像是责任链模式,但可能更难阅读(个人观点)。我认为这不是“惯用的Go语言”。虽然很有趣。 - Steven Soroka

2
在golang和其他语言中处理错误的"策略"是将错误不断地沿调用堆栈向上传递,直到你在调用堆栈中高到足以处理该错误的位置。如果你尝试过早地处理该错误,那么你很可能会重复代码。如果你处理得太晚,那么你的代码可能会出现错误。Golang使这个过程非常容易,因为它清楚地表明你在给定的位置处理错误还是传递错误。
如果你要忽略这个错误,一个简单的_符号会非常清楚地显示这一事实。如果你正在处理它,那么你将在if语句中检查确切的错误情况,从而清晰地了解你正在处理哪种错误情况。
正如上面所说,错误实际上只是一个普通的值,这样对待它就好了。

1
在行业中,大多数人都遵循golang文档错误处理和Go中提到的标准规则。这也有助于项目的文档生成。

这基本上是一个仅包含链接的答案。我建议您添加一些内容到答案中,以便如果链接失效,您的答案仍然有用。 - Neo
感谢您宝贵的评论。 - pschilakanti

0
以下是我对于在Go中减少错误处理的看法,示例是获取HTTP URL参数时使用的:
(设计模式源自https://blog.golang.org/errors-are-values
type HTTPAdapter struct {
    Error *common.AppError
}

func (adapter *HTTPAdapter) ReadUUID(r *http.Request, param string, possibleError int) uuid.UUID {
    requestUUID := uuid.Parse(mux.Vars(r)[param])
    if requestUUID == nil { 
        adapter.Error = common.NewAppError(fmt.Errorf("parameter %v is not valid", param),
            possibleError, http.StatusBadRequest)
    }
    return requestUUID
}

调用它以进行多个可能的参数如下:
    adapter := &httphelper.HTTPAdapter{}
    viewingID := adapter.ReadUUID(r, "viewingID", common.ErrorWhenReadingViewingID)
    messageID := adapter.ReadUUID(r, "messageID", common.ErrorWhenReadingMessadeID)
    if adapter.Error != nil {
        return nil, adapter.Error
    }

这不是一劳永逸的解决方案,缺点是如果出现多个错误,您只能获取最后一个错误。

但在这种情况下,重复性和风险较低,因此我只需获取最后可能出现的错误。


-1

您可以为类似的错误清理错误处理代码(因为错误是值,所以在这里必须小心),并编写一个函数,您将传递错误并处理该错误。然后您就不必每次都编写“if err!= nil {}”了。再次强调,这只会使代码更加简洁,但我认为这不是惯用的做法。

再次强调,仅仅因为您可以这样做,并不意味着您应该这样做。


-1

goerr 允许使用函数处理错误

package main

import "github.com/goerr/goerr"
import "fmt"

func ok(err error) {
    if err != nil {
        goerr.Return(err)
        // returns the error from do_somethingN() to main()
        // sequence() is terminated
    }
}

func sequence() error {
    ok(do_something1())
    ok(do_something2())
    ok(do_something3())

    return nil /// 1,2,3 succeeded
}
func do_something1() error { return nil }
func do_something2() error { return fmt.Errorf("2") }
func do_something3() error {
    fmt.Println("DOING 3")
    return nil
}

func main() {
    err_do_something := goerr.OR1(sequence)

    // handle errors

    fmt.Println(err_do_something)
}

哎呀,我认为这样复杂化/隐藏错误处理逻辑并不是一个好主意。最终的代码(需要通过goerr进行源预处理)比惯用的Go代码更难以阅读/理解。 - Dave C

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接