Opaque errors
现在我们来看第三类错误处理。 在我看来,这是最灵活的错误处理策略,因为它需要的代码和调用者之间的耦合最小。
我将这种方式称为不透明的错误处理,因为虽然您知道发生了错误,但您无法查看错误内部。 作为调用者,您对操作结果的所有了解都是有效的,或者没有。
这就是不透明的错误处理 - 只返回错误而不假设其内容。 如果采用此方式,则错误处理可以作为调试辅助工具,变得非常有用。
import “github.com/quux/bar” func fn() error { x, err := bar.Foo() if err != nil { return err } // use x }
例如,Foo
的契约不保证它将在错误的上下文中返回什么。通过传递错误附带额外的上下文,Foo
的作者现在可以自由地注释错误,而不会违反与调用者的契约。
Assert errors for behaviour, not type
在少数情况下,使用二分法(是否有错误)来进行错误处理是不够的。
例如,与进程外部的服务(例如网络活动)的交互,要求调用者查看错误的性质,以确定重试操作是否合理。
在这种情况下,我们可以断言错误实现了特定的行为,而不是断言错误是特定的类型或值。 考虑这个例子:
type temporary interface { Temporary() bool } // IsTemporary returns true if err is temporary. func IsTemporary(err error) bool { te, ok := err.(temporary) return ok && te.Temporary() }
可以将任何错误传递给 IsTemporary
以确定错误是否可以重试。
如果错误没有实现 temporary
接口; 也就是说,它没有 Temporary
方法,那么错误不是临时的。
如果错误确实实现了 Temporary
,那么如果 true
返回true ,调用者可以重试该操作。
这里的关键是,此逻辑可以在不导入定义错误的包,或者直接知道任何关于 err
的基础类型的情况下实现 - 我们只是对它的行为感兴趣。
Don’t just check errors, handle them gracefully
让我想到了第二句Go谚语,我想谈谈; 不要仅仅检查错误,优雅地处理它们。 你能用以下代码提出一些问题吗?
func AuthenticateRequest(r *Request) error { err := authenticate(r.User) if err != nil { return err } return nil }
一个明显的建议是,函数的五行可以替换为:
return authenticate(r.User)
但这是每个人都应该在代码审查中发现的简单问题。这段代码更根本的问题是无法分辨原始错误来自哪里。
如果 authenticate
返回错误,那么 AuthenticateRequest
会将错误返回给调用者,调用者也可能会这样做,依此类推。 在程序的顶部,程序的主体将错误打印到屏幕或日志文件,所有打印的都会是: No such file or directory
。
没有生成错误的文件和行的信息。 没有导致错误的调用堆栈的 stack trace
。 该代码的作者将被迫进行一个长的会话,将他们的代码二等分,以发现哪个代码路径触发了文件未找到错误。
Donovan和Kernighan的_The Go Programming Language_建议您使用 fmt.Errorf
向错误路径添加上下文
func AuthenticateRequest(r *Request) error { err := authenticate(r.User) if err != nil { return **fmt.Errorf("authenticate failed: %v", err)** } return nil }
但是正如我们之前看到的,这种模式与使用 sentinel error values
或类型断言不兼容,因为将错误值转换为字符串,将其与另一个字符串合并,然后使用 fmt.Errorf
将其转换回错误,破坏了相等性,同时完全破坏了原始错误中的上下文。
Annotating errors
我想建议一种方法来为错误添加上下文,为此,我将介绍一个简单的包。 该代码在 github.com/pkg/errors
提供。 错误包有两个主要函数:
// Wrap annotates cause with a message. func Wrap(cause error, message string) error
第一个函数是 Wrap
,它接收一个错误和一段消息,并产生一个新的错误。
// Cause unwraps an annotated error. func Cause(err error) error
第二个函数是 Cause
,它接收可能已被包装的错误,并将其解包以恢复原始错误。
使用这两个函数,我们现在可以注释任何错误,并在需要检查时恢复底层错误。 考虑一个将文件内容读入内存的函数的例子。
func ReadFile(path string) ([]byte, error) { f, err := os.Open(path) if err != nil { return nil, **errors.Wrap(err, "open failed")** } defer f.Close() buf, err := ioutil.ReadAll(f) if err != nil { return nil, **errors.Wrap(err, "read failed")** } return buf, nil }
我们将使用此函数编写一个函数来读取配置文件,然后从 main
调用它。
func ReadConfig() ([]byte, error) { home := os.Getenv("HOME") config, err := ReadFile(filepath.Join(home, ".settings.xml")) return config, **errors.Wrap(err, "could not read config")** } func main() { _, err := ReadConfig() if err != nil { fmt.Println(err) os.Exit(1) } }
如果 ReadConfig
代码路径失败,因为我们使用了 errors.Wrap
,我们在K&D样式中得到一个很好的注释错误。
could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory
因为 errors.Wrap
会产生堆栈错误,所以我们可以检查该堆栈以获取其他调试信息。 这又是一个相同的例子,但这次我们用 fmt.Println
替换 errors.Print
func main() { _, err := ReadConfig() if err != nil { errors.Print(err) os.Exit(1) } }
我们会得到如下信息:
readfile.go:27: could not read config readfile.go:14: open failed open /Users/dfc/.settings.xml: no such file or directory
第一行来自 ReadConfig
,第二行来自 ReadFile
的 os.Open
部分,其余部分来自 os
包本身,它不携带位置信息。
现在我们已经介绍了包装错误生成堆栈的概念,我们需要讨论反向操作,展开它们。 这是 errors.Cause
函数的域。
// IsTemporary returns true if err is temporary. func IsTemporary(err error) bool { te, ok := **errors.Cause(err)**.(temporary) return ok && te.Temporary() }
在操作中,每当您需要检查错误是否与特定值或类型匹配时,您应首先使用 errors.Cause
函数恢复原始错误。
Only handle errors once
最后,我想提一下:你应该只处理一次错误。 处理错误意味着检查错误值并做出决定。
func Write(w io.Writer, buf []byte) { w.Write(buf) }
如果不做决定,则忽略该错误。 正如我们在这里看到的那样,w.Write
的错误被丢弃了。
但是,针对单个错误做出多个决策也存在问题。
func Write(w io.Writer, buf []byte) error { _, err := w.Write(buf) if err != nil { // annotated error goes to log file log.Println("unable to write:", err) // unannotated error returned to caller return err } return nil }
在此示例中,如果在 Write
期间发生错误,则会将一行写入日志文件,注意错误发生的文件和行,并且错误也会返回给调用者,调用者可能会将其记录并返回,一路回到程序的顶部。
因此,您在日志文件中获得了重复的行的堆栈,但是在程序的顶部,您将获得没有原始错误的任何上下文。 有人使用Java吗?
func Write(w io.Write, buf []byte) error { _, err := w.Write(buf) return **errors.Wrap(err, "write failed")** }
使用 errors
包,您可以以人和机器都可检查的方式向错误值添加上下文。
Conclusion
总之,错误是包 public API 的一部分,对待它们就像对待 public API 的其他部分一样小心。
为了获得最大的灵活性,我建议您尝试将所有错误都视为不透明的。在不能这样做的情况下,断言行为错误,而不是类型或值错误。
最小化程序中的 sentinel error values
,并在错误发生时立即用 errors.Wrap
将其包装,从而将错误转换为不透明错误。
最后,如果需要检查,请使用 errors.Cause
恢复底层错误。
原文:don’t just check errors, handle them gracefully
本文作者 : cyningsun
本文地址 : https://www.cyningsun.com/09-09-2019/dont-just-check-errors-handle-them-gracefully-cn.html
版权声明 :本博客所有文章除特别声明外,均采用 CC BY-NC-ND 3.0 CN 许可协议。转载请注明出处!