Go語言(golang)的錯誤(error)處理的推薦方案

飛雪無情發表於2019-01-08

原文連結:www.flysnow.org/2019/01/01/…
微信公眾號:flysnow_org(飛雪無情)

對於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或者部落格網站 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 www.flysnow.org/ ,第一時間看後續精彩文章。覺得好的話,請猛擊文章右下角「好看」,感謝支援。

掃碼關注

相關文章