本系列文章並不會停留在Go語言的語法層面,更關注語言特性、學習和使用中出現的問題以及引發的一些思考。php
任何一行代碼均可能存在不可預知的問題,而這些問題就是bug的根源。爲了妥善處理這類問題,咱們須要編寫一些代碼,這類代碼被稱爲運維代碼。一般狀況下,咱們須要發現問題、判斷問題的種類、而後根據問題的種類,分別進行響應與處理。這些處理多是寫入日誌、也多是直接讓代碼中止運行,這些都視你的業務邏輯而定。這樣一來,在咱們編寫了足夠健壯的運維代碼以後,咱們便能快速的定位並解決問題。程序員
咱們先看一個最原始的錯誤處理解決方案:Unix讀取文件的API:json
int open(const char *pathname, int flags);
若是成功打開這個文件,open()會返回一個int類型的文件描述符fd;若是打開失敗,便會返回-1。爲了正確處理該函數的錯誤,咱們會編寫如下代碼:運維
if ((fd = open("/etc/hosts", O_RDONLY)) < 0) { printf("%s", "open failed") exit(1); }
這樣作會有什麼問題呢?因爲錯誤碼和正確的業務邏輯混在一個返回值裏,假如你忘了去判斷fd的值,代碼就會繼續往下執行,就會把錯誤的-1當成正確的fd,代碼就會發生不可預知的錯誤。除此以外,這種錯誤處理方式的語義也並不清晰。
那麼,咱們該如何解決這個問題呢?函數
咱們想了一下,一個返回值不行,那麼搞兩個返回值,把錯誤處理邏輯與正常邏輯區分開,代碼邏輯就會變得更加清晰了。Go語言就是這樣作的,咱們一般可以看到以下代碼:學習
func main() { res, err := json.Marshal(payload) if err != nil { return "", errors.New("序列化請求參數失敗") } }
經過將正確執行Marshal的返回結果與錯誤的返回結果分離,使代碼語義更加清晰,並且這樣會提醒程序員更加關注錯誤的返回值,而不會忘記進行錯誤處理。可是,這樣仍然存在一個問題,會出現大量的相似代碼:優化
if err != nil { // 錯誤處理邏輯 }
在Go語言的代碼中,會出現大量對err的if判斷邏輯,重複率太高,並且錯誤處理邏輯仍然和正常的代碼邏輯混淆在一塊兒,咱們若是想進一步將錯誤與正常邏輯分離,該如何作呢?spa
Java、PHP等語言提供了try-catch-finally的解決方案。日誌
try { // 正常代碼邏輯 } catch(\Exception $e) { // 錯誤處理邏輯 } finally { // 釋放資源邏輯 }
try-catch完全完成了對錯誤與正常代碼邏輯的分離。咱們用try代碼塊中包裹可能出現問題的代碼,在catch中對這些問題代碼統一進行錯誤處理。code
finally代碼塊比較特殊,它被經常用來作一些資源及句柄的釋放工做。若是沒有finally,咱們的代碼可能會像這樣:
func main() { mutex := sync.Mutex{} // 加鎖 mutex.Lock() res, err := json.Marshal("abc") if err != nil { // 釋放鎖資源 mutex.Unlock() // ....其他錯誤處理邏輯 } file, err := os.Open("abvc") if err != nil { // 釋放鎖資源 mutex.Unlock() // ....其他錯誤處理邏輯 } mutex.Unlock() }
爲了確保鎖資源在代碼結束以前必定要被釋放,咱們每次在錯誤處理邏輯中,都須要寫一次mutex.Unlock代碼,致使大量的代碼冗餘。finally代碼塊內的語句會在代碼返回或者退出以前執行,並且是百分百會執行。這樣,咱們就能夠把釋放鎖資源這一行代碼放到finally塊便可,且只用寫一次,這樣就解決了以前代碼冗餘率高的問題。在Go語言中,defer()也一樣解決了這個問題。咱們用Go中的defer語句改寫一下上述代碼:
func main() { mutex := sync.Mutex{} defer mutex.Unlock() mutex.Lock() res, err := json.Marshal("abc") if err != nil { // 錯誤處理 } file, err := os.Open("abvc") if err != nil { // 錯誤處理 } }
這就是錯誤處理的演化過程了。我我的比較喜歡Java和PHP中的try-catch-finally語法。據說Go2.0要對代碼冗餘度高的問題進行優化,咱們拭目以待吧。
接下來咱們深刻講解Go語言中的錯誤處理實現。咱們看一下以前講過的例子中,json.Marshal方法的簽名:
func Marshal(v interface{}) ([]byte, error)
咱們重點關注最後一個error類型的參數,它是一個Go語言內置的接口類型。那麼,咱們爲何要用接口類型來抽象全部的錯誤類型呢?先別急,咱們先本身想一想。
在咱們對字符串進行marshal操做的過程當中,可能會產生好多種類型的錯誤。爲了在marshal函數內部區分不一樣的錯誤類型,咱們簡單粗暴一點,可能會進行以下的處理:
func (e *encodeState) marshal(v interface{}, opts encOpts) (errorMsg string) { // 操做1可能的錯誤 if errType1 := doOp1(), errType1 != nil { err1 := errType1.getErrorMessage() // 獲取errorType1的錯誤信息 return err1 } // 操做2可能的錯誤 if errType2 := doOp2(), errType2 != nil { err2 := errType2.getErrMsg() // 方法名和errorType1不一樣 return err2 } return "" }
咱們分析一下上面這段代碼,操做doOp1可能會發生errorType1類型的錯誤,咱們要返回給調用者errorType1類型中錯誤的字符串信息;doOp2也同理。這樣作確實能夠,可是仍是有一些麻煩,咱們看看還有沒有其餘方案來優化一下。
咱們先簡單介紹一下,Go語言用一個接口類型抽象了全部錯誤類型:
type error interface { Error() string }
這個接口定義了一個Error()方法,用於返回錯誤信息,咱們先記下來,等會要用。同上個例子,咱們給以前自定義的兩種錯誤類型加點料,實現這個error接口:
type errType1 struct {} // 實現接口方法 func (*errType1) Error() { fmt.Println("我是錯誤類型1的信息") } type errType2 struct {} // 實現接口方法 func (*errType2) Error() { fmt.Println("我是錯誤類型1的信息") }
而後在marshal()函數上稍做改動,使用這兩種實現接口的錯誤類型:
func (e *encodeState) marshal(v interface{}, opts encOpts) (errorMsg string) { // 操做1可能的錯誤 if errType1 := doOp1(), errType1 != nil { return errType1.Error() } // 操做2可能的錯誤 if errType2 := doOp2(), errType2 != nil { return errType2.Error() } return "" }
你們看到優點在哪裏了嗎?在咱們調用每一個錯誤類型的返回信息方法的時候,若是用咱們一開始的方式,咱們須要進入每個錯誤類型的實現類中去翻看他的API,看看函數名是什麼;而在第二種實現方案中,因爲兩種錯誤的實現類型均實現了Error()方法,這樣,在marshal函數中若是想進行錯誤信息的獲取,咱們統一調用Error()函數,便可返回對應錯誤實現類的錯誤信息。
這其實就是一種依賴的倒置。調用方marshal()函數再也不關注錯誤類型的具體實現類,裏面有哪些方法,而轉爲依賴抽象的接口。下面給你們看一下與marshal函數相關的幾種Go語言內部定義的錯誤類型,他們均實現了error接口中的Error()方法:
第一種錯誤類型:
type MarshalerError struct { Type reflect.Type Err error sourceFunc string } func (e *MarshalerError) Error() string { srcFunc := e.sourceFunc if srcFunc == "" { srcFunc = "MarshalJSON" } return "json: error calling " + srcFunc + " for type " + e.Type.String() + ": " + e.Err.Error() }
第二種錯誤類型:
type UnsupportedValueError struct { Value reflect.Value Str string } func (e *UnsupportedValueError) Error() string { return "json: unsupported value: " + e.Str }
而其餘語言對錯誤類型的抽象化處理,有些是用繼承來實現的。如PHP中的根Exception與衆多繼承它的子異常類xxxException。在PHP中,咱們用Exception便可接收全部的異常類型,並能夠調用通用的$exception->getMessage()、$exception->getFile()等方法。
Go語言的panic和其餘語言的error有點像。若是調用了panic,代碼會馬上中止運行,一層一層向上冒泡並積累堆棧信息,直到調用棧頂結束,並打印出全部堆棧信息。
panic沒什麼好說的,而recover咱們須要好好聊一聊。recover專門用來恢復panic。也就是說,若是你在panic以前聲明瞭recover語句,那麼你就能夠在panic以後使用recover接收到panic的信息。可是問題又來了,咱們panic不是直接就退出程序了嗎,就算聲明瞭recover也執行不了呀。這個時候,咱們就須要配合defer來使用了。defer可以讓程序在panic以後,仍然執行一段收尾的代碼邏輯。這樣一來,咱們就能夠經過recover得到panic的信息,並對信息做出識別與處理了。仍然舉上述的marshal的源碼的例子,此次是真的源碼了,不是我編的:
func (e *encodeState) marshal(v interface{}, opts encOpts) (err error) { defer func() { // defer收尾 if r := recover(); r != nil { // recover恢復案發現場 if je, ok := r.(jsonError); ok { // 拿到panic的值,並轉爲錯誤來返回 err = je.error } else { panic(r) } } }() e.reflectValue(reflect.ValueOf(v), opts) return nil }
咱們看到,源碼中將defer與recover配合使用,直接改變了panic的運行邏輯。本來是panic以後會直接退出程序,這樣一來,如今程序並不會直接退出,而是被轉爲了jsonError類型,並返回。經過使用recover捕獲運行時的panic,可讓代碼繼續運行下去而不至於直接中止。
【Go語言踩坑系列(六)】面向對象
歡迎對本系列文章感興趣的讀者訂閱咱們的公衆號,關注博主下次不迷路~