我想提一下你应该只处理错误一次。 处理错误意味着检查错误值并做出单一决定。
// WriteAll writes the contents of buf to the supplied writer.
func WriteAll(w io.Writer, buf []byte) {
w.Write(buf)
}
如果你做出的决定少于一个,则忽略该错误。 正如我们在这里看到的那样, w.WriteAll
的错误被丢弃。
但是,针对单个错误做出多个决策也是有问题的。 以下是我经常遇到的代码。
func WriteAll(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
log.Println("unable to write:", err) // annotated error goes to log file
return err // unannotated error returned to caller
}
return nil
}
在此示例中,如果在 w.Write
期间发生错误,则会写入日志文件,注明错误发生的文件与行数,并且错误也会返回给调用者,调用者可能会记录该错误并将其返回到上一级,一直回到程序的顶部。
调用者可能正在做同样的事情
func WriteConfig(w io.Writer, conf *Config) error {
buf, err := json.Marshal(conf)
if err != nil {
log.Printf("could not marshal config: %v", err)
return err
}
if err := WriteAll(w, buf); err != nil {
log.Println("could not write config: %v", err)
return err
}
return nil
}
因此你在日志文件中得到一堆重复的内容,
unable to write: io.EOF
could not write config: io.EOF
但在程序的顶部,虽然得到了原始错误,但没有相关内容。
err := WriteConfig(f, &conf)
fmt.Println(err) // io.EOF
我想深入研究这一点,因为作为个人偏好, 我并没有看到 logging
和返回的问题。
func WriteConfig(w io.Writer, conf *Config) error {
buf, err := json.Marshal(conf)
if err != nil {
log.Printf("could not marshal config: %v", err)
// oops, forgot to return
}
if err := WriteAll(w, buf); err != nil {
log.Println("could not write config: %v", err)
return err
}
return nil
}
很多问题是程序员忘记从错误中返回。正如我们之前谈到的那样,Go 语言风格是使用 guard clauses
以及检查前提条件作为函数进展并提前返回。
在这个例子中,作者检查了错误,记录了它,但忘了返回。这就引起了一个微妙的错误。
Go 语言中的错误处理规定,如果出现错误,你不能对其他返回值的内容做出任何假设。由于 JSON
解析失败,buf
的内容未知,可能它什么都没有,但更糟的是它可能包含解析的 JSON
片段部分。
由于程序员在检查并记录错误后忘记返回,因此损坏的缓冲区将传递给 WriteAll
,这可能会成功,因此配置文件将被错误地写入。但是,该函数会正常返回,并且发生问题的唯一日志行是有关 JSON
解析错误,而与写入配置失败有关。
为错误添加相关内容
发生错误的原因是作者试图在错误消息中添加 context
。 他们试图给自己留下一些线索,指出错误的根源。
让我们看看使用 fmt.Errorf
的另一种方式。
func WriteConfig(w io.Writer, conf *Config) error {
buf, err := json.Marshal(conf)
if err != nil {
return fmt.Errorf("could not marshal config: %v", err)
}
if err := WriteAll(w, buf); err != nil {
return fmt.Errorf("could not write config: %v", err)
}
return nil
}
func WriteAll(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
return fmt.Errorf("write failed: %v", err)
}
return nil
}
通过将注释与返回的错误组合起来,就更难以忘记错误的返回来避免意外继续。
如果写入文件时发生 I/O
错误,则 error
的 Error()
方法会报告以下类似的内容;
could not write config: write failed: input/output error
使用 github.com/pkg/errors
包装 errors
fmt.Errorf
模式适用于注释错误 message
,但这样做的代价是模糊了原始错误的类型。 我认为将错误视为不透明值对于松散耦合的软件非常重要,因此如果你使用错误值做的唯一事情是原始错误的类型应该无关紧要的面孔
- 检查它是否为
nil
。 - 输出或记录它。
但是在某些情况下,我认为它们并不常见,您需要恢复原始错误。 在这种情况下,使用类似我的 errors
包来注释这样的错误, 如下
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
}
func ReadConfig() ([]byte, error) {
home := os.Getenv("HOME")
config, err := ReadFile(filepath.Join(home, ".settings.xml"))
return config, errors.WithMessage(err, "could not read config")
}
func main() {
_, err := ReadConfig()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
现在报告的错误就是 K&D
[11]样式错误,
could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory
并且错误值保留对原始原因的引用。
func main() {
_, err := ReadConfig()
if err != nil {
fmt.Printf("original error: %T %v\n", errors.Cause(err), errors.Cause(err))
fmt.Printf("stack trace:\n%+v\n", err)
os.Exit(1)
}
}
因此,你可以恢复原始错误并打印堆栈跟踪;
original error: *os.PathError open /Users/dfc/.settings.xml: no such file or directory
stack trace:
open /Users/dfc/.settings.xml: no such file or directory
open failed
main.ReadFile
/Users/dfc/devel/practical-go/src/errors/readfile2.go:16
main.ReadConfig
/Users/dfc/devel/practical-go/src/errors/readfile2.go:29
main.main
/Users/dfc/devel/practical-go/src/errors/readfile2.go:35
runtime.main
/Users/dfc/go/src/runtime/proc.go:201
runtime.goexit
/Users/dfc/go/src/runtime/asm_amd64.s:1333
could not read config
使用 errors
包,你可以以人和机器都可检查的方式向错误值添加上下文。 如果昨天你来听我的演讲,你会知道这个库在被移植到即将发布的 Go 语言版本的标准库中。