golang的錯誤處理一直深受大家詬病,項目裏面一半的代碼在做錯誤處理。
自己在做golang開發一段時間後,也深有同感,覺得很有必要優化一下,一方面讓代碼更優雅一些,另一方面也爲了形成系統的錯誤處理方式,而不是隨心所欲的來個errors.new(),或者一直return err。
在查閱一些資料之後,發現自己對golang錯誤處理的認識,還停留在一個低階的層面上。這裏想和大家探討一下,也爲鞏固自己所學
錯誤的返回處理
在函數多層調用時,我常用的處理方式是:
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
}
層層都加日誌非常方便故障定位,但這樣做,日誌文件中會有很多重複的錯誤描述,並且上層調用函數拿到的錯誤,還是底層函數返回的 error,沒有上下文信息
優化一:
func Write(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
// annotated error returned to caller
fmt.Errorf("authenticate failed: %v", err)
}
return nil
}
這裏去除了重複的錯誤日誌,並且在返回給上層調用函數的error信息中加上了上下文信息。但是這樣做破壞了相等性檢測,即我們無法判斷錯誤是否是一種預先定義好的錯誤。
例如:
func main() {
err := readfile(“.bashrc”)
if strings.Contains(error.Error(), "not found") {
// handle error
}
}
func readfile(path string) error {
err := openfile(path)
if err != nil {
return fmt.Errorf(“cannot open file: %v", err)
}
// ……
}
造成的後果時,調用者不得不用字符串匹配的方式判斷底層函數 readfile 是不是出現了某種錯誤。
優化二:
使用第三方庫: github.com/pkg/errors
,wrap可以將一個錯誤加上一段字符串,包裝成新的字符串。cause進行相反的操作。
// Wrap annotates cause with a message.
func Wrap(cause error, message string) error
// Cause unwraps an annotated error.
func Cause(err error) error
例如:
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
}
通過wrap即可以包含底層被調用函數的上下文信息,又可以通過cause還原錯誤,對原錯誤類型進行判斷,如下:
func main() {
_, err := ReadFile()
if errors.Cause(err) != io.EOF {
fmt.Println(err)
os.Exit(1)
}
}
今年剛發佈的go1.13新增了類似的錯誤處理函數
//go1.13 沒有提供wrap函數,但通過fmt.Errof提供了類似的功能
fmt.Errorf("context info: %w",err)
//將嵌套的 error 解析出來,多層嵌套需要調用 Unwrap 函數多次,才能獲取最裏層的 error
func Unwrap(err error) error
異常
部分開發者寫代碼中,沒有區分異常和錯誤,都統一按錯誤來處理,這種方式是不優雅的。要靈活使用Golang的內置函數panic和recover來觸發和終止異常處理流程。
錯誤指的是可能出現問題的地方出現了問題,比如打開一個文件時失敗,這種情況在人們的意料之中 ;而異常指的是不應該出現問題的地方出現了問題,比如引用了空指針,這種情況在人們的意料之外。
這裏給出一些應拋出異常的場景:
- 空指針引用
- 下標越界
- 除數爲0
- 不應該出現的分支,比如default
- 輸入不應該引起函數錯誤
在應用開發過程中,通過拋出panic異常,程序退出,及時發現問題。在部署以後,要保證程序的持續穩定運行,需要及時通過recover捕獲異常。在recover中,要用合理的方式處理異常,如:
- 打印堆棧的調用信息和業務信息,方便記錄和排查問題
- 將異常轉換爲錯誤,返回給上層調用者處理
例如:
func funcA() (err error) {
defer func() {
if p := recover(); p != nil {
fmt.Println("panic recover! p:", p)
str, ok := p.(string)
if ok {
err = errors.New(str)
} else {
err = errors.New("panic")
}
debug.PrintStack()
}
}()
return funcB()
}
func funcB() error {
// simulation
panic("foo")
return errors.New("success")
}