­

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/