Go語言(golang)的錯誤(error)處理的推薦方案
- 2020 年 2 月 10 日
- 筆記
對於Go語言(golang)的錯誤設計,相信很多人已經體驗過了,它是通過返回值的方式,來強迫調用者對錯誤進行處理,要麼你忽略,要麼你處理(處理也可以是繼續返回給調用者),對於golang這種設計方式,我們會在代碼中寫大量的if
判斷,以便做出決定。
func main() { conent,err:=ioutil.ReadFile("filepath") if err !=nil{ //錯誤處理 }else { fmt.Println(string(conent)) } }
這類代碼,在我們編碼中是非常的,大部分情況下error
都是nil
,也就是沒有任何錯誤,但是非nil
的時候,意味着錯誤就出現了,我們需要對他進行處理。
error 接口
error
其實一個接口,內置的,我們看下它的定義
// The error built-in interface type is the conventional interface for // representing an error condition, with the nil value representing no error. type error interface { Error() string }
它只有一個方法 Error
,只要實現了這個方法,就是實現了error
。現在我們自己定義一個錯誤試試。
type fileError struct { } func (fe *fileError) Error() string { return "文件錯誤" }
自定義 error
自定義了一個fileError
類型,實現了error
接口。現在測試下看看效果。
func main() { conent, err := openFile() if err != nil { fmt.Println(err) } else { fmt.Println(string(conent)) } } //只是模擬一個錯誤 func openFile() ([]byte, error) { return nil, &fileError{} }
我們運行模擬的代碼,可以看到文件錯誤
的通知。
在實際的使用過程中,我們可能遇到很多錯誤,他們的區別是錯誤信息不一樣,一種做法是每種錯誤都類似上面一樣定義一個錯誤類型,但是這樣太麻煩了。我們發現Error
返回的其實是個字符串,我們可以修改下,讓這個字符串可以設置就可以了。
type fileError struct { s string } func (fe *fileError) Error() string { return fe.s }
恩,這樣改造後,我們就可以在聲明fileError
的時候,設置好要提示的錯誤文字,就可以滿足我們不同的需要了。
//只是模擬一個錯誤 func openFile() ([]byte, error) { return nil, &fileError{"文件錯誤,自定義"} }
恩,可以了,已經達到了我們的目的。現在我們可以把它變的更通用一些,比如修改fileError
的名字,再創建一個輔助函數,便於我們創建不同的錯誤類型。
//blog:www.flysnow.org //wechat:flysnow_org func New(text string) error { return &errorString{text} } type errorString struct { s string } func (e *errorString) Error() string { return e.s }
變成以上這樣,我們就可以通過New
函數,輔助我們創建不同的錯誤了,這其實就是我們經常用到的errors.New
函數,被我們一步步剖析演化而來,現在大家對Go語言(golang)內置的錯誤error
有了一個清晰的認知了。
存在的問題
雖然Go語言對錯誤的設計非常簡潔,但是對於我們開發者來說,很明顯是不足的,比如我們需要知道出錯的更多信息,在什麼文件的,哪一行代碼?只有這樣我們才更容易的定位問題。
還有比如,我們想對返回的error
附加更多的信息後再返回,比如以上的例子,我們怎麼做呢?我們只能先通過Error
方法,取出原來的錯誤信息,然後自己再拼接,再使用errors.New
函數生成新錯誤返回。
如果我們以前做過java開發,我們知道Java的異常是可以嵌套的,也就是說,通過這個,我們很容易知道錯誤的根本原因,因為Java的異常,是一層層的嵌套返回的,不管中間經歷了多少包裝,我們可以通過cause
找到根本錯誤的原因。
解決問題
如果要解決以上的問題,那麼首先我們必須再繼續擴充我們的errorString
,再增加一些字段來存儲更多的信息。比如我們要記錄堆棧信息。
type stack []uintptr type errorString struct { s string *stack }
歡迎關注微信公眾號flysnow_org
或者博客網站 https://www.flysnow.org/ 查看更多原創文章。
有了存儲堆棧信息的stack
字段,我們在生成錯誤的時候,就可以把調用的堆棧信息存儲在這個字段里。
//blog:www.flysnow.org //wechat:flysnow_org func callers() *stack { const depth = 32 var pcs [depth]uintptr n := runtime.Callers(3, pcs[:]) var st stack = pcs[0:n] return &st } func New(text string) error { return &errorString{ s: text, stack: callers(), } }
完美解決,現在如果再解決,對現有的錯誤附加一些信息的問題呢?相信大家應該有思路了。
type withMessage struct { cause error msg string } func WithMessage(err error, message string) error { if err == nil { return nil } return &withMessage{ cause: err, msg: message, } }
使用WithMessage
函數,對原來的error
包裝下,就可以生成一個新的帶有包裝信息的錯誤了。
推薦的方案
以上我們在解決問題是,採取的方法是不是比較熟悉?尤其是看源代碼,沒錯,這就是github.com/pkg/errors
這個錯誤處理庫的源代碼。
因為Go語言提供的錯誤太簡單了,以至於簡單的我們無法更好的處理問題,甚至不能為我們處理錯誤,提供更有用的信息,所以誕生了很多對錯誤處理的庫,github.com/pkg/errors
是比較簡潔的一樣,並且功能非常強大,受到了大量開發者的歡迎,使用者很多。
它的使用非常簡單,如果我們要新生成一個錯誤,可以使用New
函數,生成的錯誤,自帶調用堆棧信息。
func New(message string) error
如果有一個現成的error
,我們需要對他進行再次包裝處理,這時候有三個函數可以選擇。
//只附加新的信息 func WithMessage(err error, message string) error //只附加調用堆棧信息 func WithStack(err error) error //同時附加堆棧和信息 func Wrap(err error, message string) error
其實上面的包裝,很類似於Java的異常包裝,被包裝的error
,其實就是Cause
,在前面的章節提到錯誤的根本原因,就是這個Cause
。所以這個錯誤處理庫為我們提供了Cause
函數讓我們可以獲得最根本的錯誤原因。
func Cause(err error) error { type causer interface { Cause() error } for err != nil { cause, ok := err.(causer) if !ok { break } err = cause.Cause() } return err }
使用for
循環一直找到最根本(最底層)的那個error
。
以上的錯誤我們都包裝好了,也收集好了,那麼怎麼把他們裏面存儲的堆棧、錯誤原因等這些信息打印出來呢?其實,這個錯誤處理庫的錯誤類型,都實現了Formatter
接口,我們可以通過fmt.Printf
函數輸出對應的錯誤信息。
%s,%v //功能一樣,輸出錯誤信息,不包含堆棧 %q //輸出的錯誤信息帶引號,不包含堆棧 %+v //輸出錯誤信息和堆棧
以上如果有循環包裝錯誤類型的話,會遞歸的把這些錯誤都會輸出。
小結
通過使用這個 github.com/pkg/errors
錯誤庫,我們可以收集更多的信息,可以讓我們更容易的定位問題。
我們收集的這些信息不止可以輸出到控制台,也可以當做日誌,使用輸出到相應的Log
日誌里,便於分析問題。
據說這個庫,會被加入到Golang 標準 SDK 里,期待着,如果加入的話,應該就是補充現在標準庫里的errors
這個package了。
本文為原創文章,轉載註明出處,歡迎掃碼關注公眾號
flysnow_org
或者網站asf http://www.flysnow.org/