爭議最多的錯誤處理,Go 2 將會進行改進,如下草案你滿意嗎?

2019-08-21   Go語言中文網

Go 2的總體目標是在輔助工程擴展為大的代碼基線時做到遊刃有餘。

通常,我們的Go程序有很多錯誤檢查,但缺少錯誤處理。我們通常使用如下代碼所示的賦值判斷語句進行錯誤檢查。

if _, err := io.Copy(w, r); nil != err {
return err
}

這樣寫起來較繁瑣,設計草案旨在引入一種輕量的語法來進行錯誤檢查以解決當前的這些問題。

1 當前問題

Go 使用的是對顯式錯誤結果的顯式錯誤檢查,而其他異常處理型語言(諸如C++,C#,Java等)使用的是對隱式結果進行隱式檢查。

對於異常處理型語言的處理方式,因我們全然看不到隱式檢查,所以難以驗證程序是否正確恢復到檢查失敗時的狀態。

下面是一個錯誤檢查較完整的文件拷貝代碼,其錯誤處理的重點在於當io.Copy或w.Close失敗時,應移除寫了一半的dst文件。

該代碼較健壯,但不夠整潔,也不夠優雅。

2 目標

減少大量錯誤檢查代碼,使錯誤檢查更輕量,使錯誤處理更便捷。

不重蹈異常處理的覆轍,錯誤檢查及錯誤處理應繼續保持顯式的方式。

兼容現有代碼。

3 草案概覽

設計草案引入了兩個新的關鍵字,check與handle,分別進行錯誤檢查與錯誤處理。

使用check f(x, y, z)或check err進行顯式錯誤檢查。

使用hande語句進行錯誤處理器的定義。

當錯誤檢查失敗時,其轉向到最裡邊的Handler,最裡邊的Handler又轉向到其上的下一個Handler,直至某一個Handler執行了return語句。

例如,依照設計草案,如上代碼可以改進為更簡短的方式:

4 草案詳情

check

check可用於error類型的表達式或者返回一組值且最後一個值為error類型的函數調用。

給定變量v1, v2, ..., vN, vErr,

v1, ..., vN := check /expr/

其等價於:

v1, ..., vN, vErr := /expr/
if vErr != nil {
/error result/ = handlerChain(vn)
return
}

vErr必須為error類型。

類似,

foo(check /expr/)

等價於:

v1, ..., vN, vErr := /expr/
if vErr != nil {
/error result/ = handlerChain(vn)
return
}
foo(v1, ..., vN)

如下是一段常規的錯誤處理代碼:

其可被改寫為:

func printSum(a, b string) error {
handle err { return err }
fmt.Println("result:", check strconv.Atoi(x) + check strconv.Atoi(y))
return nil
}

通常需要包裝下錯誤信息的上下文,代碼可以寫作:

func printSum(a, b string) error {
handle err {
return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
}
fmt.Println("result:", check strconv.Atoi(x) + check strconv.Atoi(y))
return nil
}

Handler

Handler用來處理check所發現的錯誤,Handler使用return語句可以使對應函數即刻退出。

不帶返回值的return僅可用於無返回值函數或變量聲明式函數,對變量聲明式函數,其返回這些變量的當前值。

Handler鏈函數帶一個error類型的參數且與對應函數的返回值定義相同。

每個check對應哪個Handler鏈函數取決於check所定義的範圍。

拿如下代碼舉例:

Check 1:在循環內,依序運行Handler C、B及A。不同於defer,定義在循環內的Handler不會因每次新的疊代而累積

Check 2:在函數末尾,僅運行Handler A。

幾個重要點:

check到錯誤,即會落入Handler,無法再回到對應函數的控制;

Handler執行總是在defer語句之前;

若對應函數需要有返回值,但check的Handler鏈函數沒有return語句會引起編譯錯誤。

默認Handler

默認Handler隱式定義在最後一個參數是error類型的函數的頭部。

依賴默認Handler,printSum函數可以寫作:

func printSum(a, b string) error {
x := check strconv.Atoi(a)
y := check strconv.Atoi(b)
fmt.Println("result:", x + y)
return nil
}

總結

1)Handler

a)僅需一個error類型的參數;

b)與對應函數的返回參數相同。

2)handle語句

a)Handler使用return會將對應函數返回;

b)對應函數使用參數聲明式返回,一個空的return語句會返回這些參數的當前值。

3)check表達式

a)若check用在僅返回一個error值的函數前面,check會消費該值,且不會生產任何結果;

b)一個check的Handler鏈會依Handler在當前作用的域的定義序的反序執行,直至某個Handler return;

c)check表達式不可用於Handler。

4)默認Handler

a)對應函數非參數聲明式返回,默認Handler會返回排頭參數的0值及最後參數的error值;

b)對應函數為參數聲明式返回,默認Handler會返回排頭參數的當前值及最後參數的error值;

c)因默認Handler定義在函數頭部,其是Handler鏈的最後一環。

重點:

Handler鏈調用類似於函數調用,check到錯誤的位置被保存為Handler的調用者棧幀。

參考資料

[1] https://github.com/golang/proposal/blob/master/design/go2draft-error-handling-overview.md

[2] https://github.com/golang/proposal/blob/master/design/go2draft-error-handling.md

原文連結:https://leileiluoluo.com/posts/go2-error-handling-draft-design.html

本文作者:磊磊落落的博客,原創授權發布