7.2. 错误只处理一次

最后,我想提一下你应该只处理错误一次。 处理错误意味着检查错误值并做出单一决定。

  1. // WriteAll writes the contents of buf to the supplied writer.
  2. func WriteAll(w io.Writer, buf []byte) {
  3. w.Write(buf)
  4. }

如果你做出的决定少于一个,则忽略该错误。 正如我们在这里看到的那样, w.WriteAll 的错误被丢弃。

但是,针对单个错误做出多个决策也是有问题的。 以下是我经常遇到的代码。

  1. func WriteAll(w io.Writer, buf []byte) error {
  2. _, err := w.Write(buf)
  3. if err != nil {
  4. log.Println("unable to write:", err) // annotated error goes to log file
  5. return err // unannotated error returned to caller
  6. }
  7. return nil
  8. }

在此示例中,如果在 w.Write 期间发生错误,则会写入日志文件,注明错误发生的文件与行数,并且错误也会返回给调用者,调用者可能会记录该错误并将其返回到上一级,一直回到程序的顶部。

调用者可能正在做同样的事情

  1. func WriteConfig(w io.Writer, conf *Config) error {
  2. buf, err := json.Marshal(conf)
  3. if err != nil {
  4. log.Printf("could not marshal config: %v", err)
  5. return err
  6. }
  7. if err := WriteAll(w, buf); err != nil {
  8. log.Println("could not write config: %v", err)
  9. return err
  10. }
  11. return nil
  12. }

因此你在日志文件中得到一堆重复的内容,

  1. unable to write: io.EOF
  2. could not write config: io.EOF

但在程序的顶部,虽然得到了原始错误,但没有相关内容。

  1. err := WriteConfig(f, &conf)
  2. fmt.Println(err) // io.EOF

我想深入研究这一点,因为作为个人偏好, 我并没有看到 logging 和返回的问题。

  1. func WriteConfig(w io.Writer, conf *Config) error {
  2. buf, err := json.Marshal(conf)
  3. if err != nil {
  4. log.Printf("could not marshal config: %v", err)
  5. // oops, forgot to return
  6. }
  7. if err := WriteAll(w, buf); err != nil {
  8. log.Println("could not write config: %v", err)
  9. return err
  10. }
  11. return nil
  12. }

很多问题是程序员忘记从错误中返回。正如我们之前谈到的那样,Go 语言风格是使用 guard clauses 以及检查前提条件作为函数进展并提前返回。

在这个例子中,作者检查了错误,记录了它,但忘了返回。这就引起了一个微妙的错误。

Go 语言中的错误处理规定,如果出现错误,你不能对其他返回值的内容做出任何假设。由于 JSON 解析失败,buf 的内容未知,可能它什么都没有,但更糟的是它可能包含解析的 JSON 片段部分。

由于程序员在检查并记录错误后忘记返回,因此损坏的缓冲区将传递给 WriteAll,这可能会成功,因此配置文件将被错误地写入。但是,该函数会正常返回,并且发生问题的唯一日志行是有关 JSON 解析错误,而与写入配置失败有关。