Go语言中的错误返回
在Go语言中有两种基础的错误返回方式:
1 | func (c Config) Save() error { |
或者
1 | func (c Config) Save() error { |
第一种方式把原始的错误栈顶部信息传递出来,但是没有加入上下文信息。因此,你的Save方法可能会以打印出“file not found:default.cfg”结束,而不告诉调用者为什么它要尝试打开default.cfg。
第二种方式允许你添加上下文到错误中,所以上面的错误可以变成这样:“can’t find default config file: file not found: default.cfg”。这样错误就有了一个上下文环境,但是不幸的是,它同时也创建了一个只有原始错误文字解释的全新错误。这对人来说很友好,但是对代码错误处理来说没有帮助。
如果你使用第一种代码实现,在调用之后,可以用os.IsNotExist()来判断是否有一个not found错误,然后根据情况创建一个文件。
如果使用第二种代码实现,错误类型就不是os.Open返回的那种了,因此os.IsNotExist不会返回true。使用fmt.Errorf掩盖了原始错误。
有些时候掩盖原始错误可能是好的,如果你不想调用方依赖于实现细节。然而,大部分时候你可能需要给你的调用方提供审视你返回的错误的能力,并且对错误作出处理。然而,这样又会使得错误信息没有上下文,所以调用你的代码的人就得费些脑力看下具体实现,搞清楚这个错误到底是什么意思。
对于上面这两种方案来说,有一个更严重的问题是当你debug的时候,你根本不知道错误从哪里来的。没有完整的错误栈,甚至连哪个文件的哪一行都不知道。除非你非常小心的维护,让你的错误信息容易捕捉,否则,debug的时候是非常的困难的。我都数不清有多少次去搜索错误格式信息,然后希望自己猜到的是对的格式。
没办法,这就是Go的世界,所以作为开发者我们应该怎么做呢?当然是写一个更好的错误处理包啊。并且别人已经写了很多错误处理的包了。大部分都封装了原始的错误,所以你可以添加上下文信息并且保持了原始的错误信息类型,所以你可以用os.IsNotExist之类的方法来检查错误。在Canonical,Juju组就写了一个这种错误处理包(实际上是3个,然后让他们自由竞争,最后只有一个胜出,留了下来),地址:https://github.com/juju/errors.
因此,你可以像这样返回错误:
1 | func (c Config) Save() error { |
这段代码返回了一个错误处理包创建的新的错误,并且在原始错误的错误消息之前加上了一段用户给的错误消息(就像fmt.Errorf一样),但是你可以用errors.Cause(err)
来访问到原始的错误。因此你可以使用os.IsNotExist(errors.Cause(err))
来检查错误。
然而,这个包和其它的错误处理包一样被同一个问题折磨:你的包只能理解自己定义的错误,其它代码无法正确解析你的错误(因为它们不会用errors.Cause来获取原始错误)。现在你又回到了开头:你的错误就像fmt.Errorf创建的错误一样对第三方是不透明的。
关于这个问题我也没有明确的答案。它是Go语言的标准错误类型天生的特性(或者说缺陷)。
很明显,如果你在写一个独立的package给很多其他的人使用的话,不要使用第三方的错误包。你的使用者不太可能会使用同样的错误处理包,所以他们无法开箱即用,并且这会给你的代码加入不必要的依赖。在两种返回错误的方式中抉择是一件艰难的事情。一方面,很难知道什么时候原始错误中的信息是对你的调用者有用的。另一方面,使用fmt.Errorf创建的错误可以把难以琢磨的错误变得明显。
如果你在写一个应用,并且你可以控制大部分的package,那么使用错误处理包就有用了。但是仍然存在你的自定义错误会传递到第三方的风险,而他们不知道如何解析这个错误。另外,任何第三方错误处理包都增加了代码的复杂度,你必须记得调用了errors.Cause(err)
来获取原始错误,否则判断就会出差错。
你必须在这三个选项中选择一个错误的返回方式。请小心的选择。有时候你可能会选择了一个让你更难受的方式。