不一樣的go語言-error


前言

  go語言的error處理方式,在目前流行的編程語言中屬於刺頭。似乎天生就是用來有別於他人標記。TIOBE排行榜全十除了C語言,無一例外是try catch的陣營。而排在go之前的語言除了C與perl外,同樣是try catch的忠實擁躉。那么go的設計者為什么要這么做呢,只是為博人眼球嗎?

關於error

  在go語言的定義中,error不一定表示一個錯誤,它也可以表示其他信息。在標准庫中可以看到如文件尾io.EOF的定義,而第三方庫中亦有如jdbc驅動中的sql.ErrNoRows的使用,由此可見,在go中error完全可以看作是一種特殊的返回值,以幫助調用方獲知被調用函數的執行情況而決定后續的代碼邏輯。error的設計,按照流行的說辭就是讓程序員面向異常編程,腦海里要時刻記得處理error。雖然這並沒有什么不對,畢竟現在也提倡防御性編程,但就因為eerror的籠統且go支持多返回值,導致錯誤處理代碼嚴重影響正常的業務邏輯,多返回值帶來的判斷組合成倍的增長。我認為go的error設計並沒有擺脫C語言的影子,甚至只是換了個方式來表達而已,仍然是值模式。此時的go 2.0草案已着手解決這個問題,只是終究還是回歸了類try catch模式。

//當前go 1的錯誤處理方式
f, err := os.Open(fileName)
if err != nil {
    // handle error here
}
//使用GO 2草案中的錯誤處理方式
func CopyFile(src, dst string) error {
	handle err {//類try-catch模式
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}

	r := check os.Open(src)
	defer r.Close()

	w := check os.Create(dst)
	handle err {
		w.Close()
		os.Remove(dst) // (only if a check fails)
	}

	check io.Copy(w, r)
	check w.Close()
	return nil
}

最佳實踐

  吐槽歸吐槽,但還是要回歸主題。go的異常處理由error、panic、recover組成,可在作用上等同於java的Exception、throw、catch/finally。異常處理是任何一門編程語言都需要考慮如何處理的事情,特別是對於想要成為系統性、項目性的語言。go的異常處理的特殊性使得其在誕生之初,就被吐槽得最多,因而也被研究實踐得最多。不同的人使用同一種語言敲出的代碼可以有完全不一樣的感覺,自然也就有良莠之分。所以語言永遠都只是一種工具,如何使用工具及如何用好工具才是關鍵。就像用java拋出Exception,當throws的細分異常超過一個時,你會立刻馬上想到拋出一個大Exception。這就是工具的濫用,相信設計者最初的目的決不在此。java異常的嚴謹性在於可以讓調用者清楚地看到將要調用的方法的可控性,無異常拋出聲明的方法可以放心調用,不需要處理異常;反正則可以知道被調用的方法會拋出哪些細分的異常,然后在調用的地方小心地處理這些異常(雖然java的try-catch語法很啰嗦,也很丑)。當然這一切都建立在所有的團隊成員都深刻地認識到異常的最佳實踐的前提下。go也不例外,也有一些最佳實踐。

  java將異常分為錯誤(error)、未檢視異常(unchecked exception)、已檢視異常(checked exception),未檢視、已檢視異常這兩個概念即使多年的老鳥也依然分不清楚,包括我在內。其實簡單地划分就是當前可妥善處理或當前無能為力。已檢視異常屬於當前可妥善處理的范圍,即
程序可對這類異常提供分支、恢復等處理方案的異常,比如FileNotFoundException,解決方案可以是讀取備選文件或者跳過;而error、未檢視異常則屬於當前無能為力的異常(在寫代碼的時候不知道或者意料不到),這類異常通常是在運行時才能發現且通常是程序員的錯或者依賴的運行環境異常(如操作系統、JVM)。而go在規范上則不區分,並且建議要妥善處理每一個方法返回的異常,哪怕是只打印一行日志。一個典型的go處理異常的例子如下:

package main

import (
    "fmt"
	"github.com/go-redis/redis"
)

func main() {
    defer func() {
        //嘗試處理panic
        //注意: recover只在defer函數中有效
        if p := recover(); p != nil {
            err, ok := p.(error)
            if ok {
                //此處只是簡單的打印error
                fmt.Printf("panic, %s\n", err.Error())
            }
            //打印異常堆棧
			fmt.Printf("panic stack: %s\n", string(debug.Stack()))
		} 
    }()
    //連接redis
    client := redis.NewClient(&redis.Options{
    	Addr: "localhost",
    	DB: 0,
    	MinIdleConns: 10,
    })
    pong, err := client.Ping().Result()
    if err != nil {
        //異常發生, 程序中斷執行,轉而執行defer聲明的函數
    	panic(err)
    }
    fmt.Printf("連接redis成功, ping: %s\n", pong)
}

  從上述代碼中可以看到,如果調用了很多可能發生錯誤的方法,整個代碼視界內,將會出現一堆的if err!= nil這樣的語句,然后里面就一個打印錯誤日志的語句。總之讓人有點不爽,但習慣了其實也還好。那么對於自己編寫的方法,應該如何定義error呢?

最佳實踐 方式 說明
位置 最后 返回值列表最后一個值且不要返回多個error
統一 同類型的錯誤,消息體保持一致 使用error.New創建error或者使用pkg/errors包
二值 bool 布爾邏輯類的方法使用bool代替error
釋放資源 在操作資源時,定義defer函數用於資源釋放 defer函數會被按先后順序入棧,執行時按相反的順序出棧執行
非法分支 按業務邏輯不應該出現的分支可使用panic中止執行,並交由recover處理 -
非法參數 對於Must類開頭的函數,使用panic 這種設計可以避免調用方處理error,但調用方需小心處理panic,運行的程序不應該因為panic崩潰

  我認為異常處理的關鍵在於如何告知調用方你寫的函數出錯了,哪里出錯了,出了什么錯,然后交由調用方處理(語言給調用方提供工具中斷程序或恢復異常等方式)。在這個指導方針下,我覺得java顯然對調用方更為友好,從方法聲明就可以知道方法會拋出哪些異常,不需要查看源代碼,然后一個大代碼塊的try,catch各個細分的Exception(不分青紅皂白直接catch Exception的程序員不是好程序員)。

  除此之外,go有很多增加魔力值的點,比如type、defer、類型推斷、閉包、channel、無括號、cgo,不足之處在於標准庫太弱、沒有泛型、切片與數組易混。此外,go沒有函數重載,雖然這並不是什么大問題。

請關注公眾號

不一樣的go語言


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM