go errors
Go语言中的错误处理(Error Handling in Go)
更优雅的 Golang 错误处理
使用“隐藏内部细节的错误处理”
使用errors.Wrap封装原始error
使用errors.Cause找出原始error
为了行为而断言,而不是类型
尽量减少错误值的使用
- Annotating errors
我想建议一种方法来为错误添加上下文,并且我会介绍一个简单的包。该代码在:http://github.com/pkg/errors。错误包有两个主要功能:
// Wrap annotates cause with a message.
func Wrap(cause error, message string) error
第一个函数是 Wrap,它接受一个 error 接口类型和一个 string,并返回新的 error 接口类型。
// 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 产生了一堆错误,我们可以检查该堆栈以获取更多调试信息。这也是同样的例子,但这次我们用 errors.Print 换 fmt.Println。
func main(){
_,err:= ReadConfig()
如果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,第二来自 os.Open 的一部分 ReadFile,剩余部分来自于 os 包本身,其不携带位置信息。我们引入了包装错误的概念来生成一个堆栈,现在我们需要讨论相反的事情,结构 error,这是该 errors.Cause 功能的领域。
为了行为断言错误,而非为了类型
在有些场景下,仅仅知道是否出错是不够的。比如,和进程外其它服务通信,需要了解错误的属性,以决定是否需要重试操作。
这种情况下,就不要判断错误值或者错误的类型(如os.PathError)了,我们可以判断错误是否实现某个行为。
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()
}
这种实现方式的好处在于,不需要知道具体的错误类型,也就不需要引用定义了错误类型的三方package。如果你是底层代码的开发者,哪天你想更换一个实现更好的error,也不用担心影响上层代码逻辑。如果你是上层代码的开发者,你只需要关注error是否实现了特定行为,不用担心引用的三方package升级后,程序逻辑失败。
在操作中,无论何时需要检查错误与特定值或类型相匹配时,都应首先使用该 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 将一行写入日志文件,注意发生错误的文件和行,并且错误也会返回给调用者,调用者可能会将其记录并返回,回到程序的顶端。因此,你会在日志文件中获得一堆重复行,但是在程序的顶部,没有得到任何上下文的原始错误。anyone?
func Write(w io.Write, buf []byte) error {
_, err := w.Write(buf)
return errors.Wrap(err, "write failed")
}
使用该 http://github.com/pkg/errors 软件包,你可以通过人员和机器均可检查的方式向错误值添加上下文
- 结论
简而言之,错误是你的软件包的公共 API 的一部分,与你公开 API 的任何其他部分一样谨慎。为了获得最大的灵活性,我建议你尝试将所有错误视为不透明。在不能这样做的情况下,为行为声明错误,而不是类型或值。最大限度地减少程序中的标记错误值的数量,并将错误转换为不透明的错误,只要它们一出现就用 errors.Wrap 将它们包装起来。最后,如果需要检查它,则使用 errors.Cause 来恢复基本错误。
原文:
Don’t just check errors, handle them gracefullydave.cheney.net