天天看點

Golang中錯誤處理的建議

該文章摘取自 Go 語言實戰: 編寫可維護 Go 語言代碼建議 ,是llitfkitfk(田浩)在github上翻譯自Dave大神的Practical Go: Real world advice for writing maintainable Go programs 如有侵權,請聯系删除,謝謝

7.2. 錯誤隻處理一次

最後,我想提一下你應該隻處理錯誤一次。 處理錯誤意味着檢查錯誤值并做出單一決定。

// 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 解析錯誤,而與寫入配置失敗有關。

7.2.1. 為錯誤添加相關内容

發生錯誤的原因是作者試圖在錯誤消息中添加 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
           

7.2.2. 使用 github.com/pkg/errors 包裝 errors

fmt.Errorf 模式适用于注釋錯誤 message,但這樣做的代價是模糊了原始錯誤的類型。 我認為将錯誤視為不透明值對于松散耦合的軟體非常重要,是以如果你使用錯誤值做的唯一事情是原始錯誤的類型應該無關緊要的面孔

  1. 檢查它是否為 nil。
  2. 輸出或記錄它。

但是在某些情況下,我認為它們并不常見,您需要恢複原始錯誤。 在這種情況下,使用類似我的 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 語言版本的标準庫中。