nnotates cause with a message.
func Wrap(cause error, message string) error
// Cause unwraps an annotated error.
func Cause(err error) error
通过 Wrap
可以将一个错误,加上一个字符串,“包装”成一个新的错误;通过 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
}
这是一个读文件的函数,先尝试打开文件,如果出错,则返回一个附加上了 “open failed” 的错误信息;之后,尝试读文件,如果出错,则返回一个附加上了 “read failed” 的错误。
当在外层调用 ReadFile
函数时:
func main() {
_, err := ReadConfig()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
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")
}
这样我们在 main 函数里就能打印出这样一个错误信息:
could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory
它是有层次的,非常清晰。而如果我们用 pkg/errors
库提供的打印函数:
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
上面讲的是 Wrap
函数,接下来看一下 “Cause” 函数,以前面提到的 temporary
接口为例:
type temporary interface {
Temporary() bool
}
// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
te, ok := errors.Cause(err).(temporary)
return ok && te.Temporary()
}
判断之前先使用 Cause
取出错误,做断言,最后,递归地调用 Temporary
函数。如果错误没实现 temporary
接口,就会断言失败,返回 false
。
Only handle errors once
什么叫“处理”错误:
Handling an error means inspecting the error value, and making a decision.
意思是查看了一下错误,并且做出一个决定。
例如,如果不做任何决定,相当于忽略了错误:
func Write(w io.Writer, buf []byte) {? w.Write(buf)?
w.Write(buf)?
}
w.Write(buf)?
会返回两个结果,一个表示写成功的字节数,一个是 error,上面的例子中没有对这两个返回值做任何处理。
下面这个例子却又处理了两次错误:
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 err
}?
return nil
}
第一次处理是将错误写进了日志,第二次处理则是将错误返回给上层调用者。而调用者也可能将错误写进日志或是继续返回给上层。
这样一来,日志文件中会有很多重复的错误描述,并且在最上层调用者(如 main 函数)看来,它拿到的错误却还是最底层函数返回的 error,没有任何上下文信息。
使用第三方的 error 包就可以比较完美的解决问题:
func Write(w io.Write, buf []byte) error {?
_, err := w.Write(buf)?
return errors.Wrap(err, "write failed")?
}
返回的错误,对于人和机器而言,都是友好的。
小结
这一部分主要讲了处理 error 的一些原则,引入了第三方的 errors 包,使得错误处理变得更加优雅。
作者最后给出了一些结论:
- errors 就像对外提供的 API 一样,需要认真对待。
- 将 errors 看成黑盒,判断它的行为,而不是类型。
- 尽量不要使用 sentinel errors。
- 使用第三方的错误包来包裹 error(errors.Wrap),使得它更好用。
- 使用 errors.Cause 来获取底层的错误。
胎死腹中的 try 提案
之前已经出现用 “check & handle” 关键字和 “try 内置函数”改进错误处理流程的提案,目前 try 内置函数的提案已经被官方提前拒绝,原因是社区里一边倒地反对声音。
关于这两个提案的具体内容见参考资料【check & handle】和【try 提案】。
go 1.13 的改进
有一些 Go 语言失败的尝试,比如 Go 1.5 引入的 vendor 和 internal 来管理包,最后被滥用而引发了很多问题。因此 Go 1.13 直接抛弃了 GOPATH
和 vendor
特性,改用 module
来管理包。
柴大在《Go 语言十年而立,Go2 蓄势待发》一文中表示:
比如最近 Go 语言之父之一 Robert Griesemer 提交的通过 try 内置函数来简化错误处理就被否决了。失败的尝试是一个好的现象,它表示 Go 语言依然在一些新兴领域的尝试 —— Go 语言依然处于活跃期。
今年 9 月 3 号,Go 发布 1.13 版本,除了 module 特性转正之外,还改进了数字字面量。比较重要的还有 defer 性能提升 30%,将更多的对象从堆上移动到栈上以提升性能,等等。
还有一个重大的改进发生在 errors 标准库中。er