隨著 Go1.18 泛型的發(fā)布,原先矛盾最深的泛型已經(jīng)得到一個初步的解決方案。在社區(qū)調(diào)研上,開發(fā)者在使用 Go 時面臨的最大挑戰(zhàn)已經(jīng)轉(zhuǎn)移到了錯誤處理上,需要投入精力去 “解決” 它。
大家好,我是煎魚。
今天煎魚和大家一起打開來看看,這能把 Go 錯誤處理機(jī)制給掀開重整不。
背景
來自 PingCAP 的提案作者 @Greg Weber 會干這事基于兩個因素,一個是在《Go Developer Survey 2022 Q2 Results[2]》中明確提到。

隨著 Go1.18 泛型的發(fā)布,原先矛盾最深的泛型已經(jīng)得到一個初步的解決方案。在社區(qū)調(diào)研上,開發(fā)者在使用 Go 時面臨的最大挑戰(zhàn)已經(jīng)轉(zhuǎn)移到了錯誤處理上,需要投入精力去 “解決” 它。
另外一個因素就是眾所皆知的,Go 錯誤處理代碼比較繁瑣,常被工程師們戲稱一個 Go 工程里有 30% 都 if err = nil。
如下代碼:
_, err := f()
if err != nil {
...
}
_, err = r()
if err != nil {
...
}
_, err = w()
if err != nil {
...
}
希望讓其更優(yōu)雅。也有許多小伙伴認(rèn)同這個設(shè)計(jì),確實(shí)是簡單、直觀的處理,在社區(qū)形成了角力。
try-handler 提案
本次提案中所提到的解決方案,是增加一個新語句 try ,以此達(dá)到簡潔的錯誤處理的作用,讓 if err != nil 的處理絲滑起來。
如下代碼:
編譯器翻譯后生成的代碼:
if err != nil {
return handler(err)
}
在函數(shù)中可以如下:
func(args...) (rtype1, rtypes..., rtypeN, error) {
try err, handler
...
}
翻譯后生成的代碼:
func(args...) (rtype1, rtypes..., rtypeN, error) {
if err != nil {
return Zero(rtype1), Zeros(rtypes...)..., Zero(rtypeN), handler(err)
}
...
}
也可以只針對 if err != nil 處理。如下代碼:
翻譯后生成的代碼:
if err != nil {
return err
}
不會調(diào)用不存在的 handler 進(jìn)行處理,將會直接返回。三行(if err != nil 的邏輯)直接變 3 個單詞(try)。
不想寫函數(shù),也可以直接:
x, err := f()
try err, fmt.Errorf("f fail: %w", err)
針對 defer+try 的場景可以如下:
func CopyFile(src, dst string) error {
defer try func(err error) error {
return fmt.Errorf("copy %s %s: %w", src, dst, err)
}
...
}
入?yún)⑹潜容^靈活的,作者希望它是泛型,這樣能夠適配各種場景的要求。
示例和實(shí)踐
針對本提案,原作者給出了各類使用場景的示例。如下代碼:
import (
"fmt"
)
// This helper should be defined in the fmt package
func Handlew(format string, args ...any) func(error) error {
return func(err error) error {
args = append(args, err)
return fmt.Errorf(format+": %w", args...)
}
}
// This helper should be defined in the fmt package
func Handlef(format string, args ...any) func(error) error {
return func(err error) error {
args = append(args, err)
return fmt.Errorf(format+": %v", args...)
}
}
func valAndError() (int, error) {
return 1, fmt.Errorf("make error")
}
func newGo() (int, error) {
x, err := valAndError()
try err
// Common formatting functions will already be provided
i := 2
x, err = valAndError()
try err, Handlew("custom Error %d", i)
// Using a custom error type
// For convenience the error type can expose a method to set the error
x, err = valAndError()
try err, TheErrorAsHandler(i)
}
type TheError struct{
num int
err error
}
func (t TheError) Error() String {
return fmt.Sprintf("theError %d %v", t.num, t.err)
}
func TheErrorAsHandler(num int) func(err) TheError {
return func(err error) TheError {
return theError{ num: i, err: err }
}
}
另外在日常的 Go 工程中,提案作者認(rèn)為 CopyFile 函數(shù)是新提案語句的一種很好的實(shí)踐。為此基于 try-handler 進(jìn)行了一版改造和說明。
如下代碼:
// This helper can be used with defer
func handle(err *error, handler func(err error) error) {
if err == nil {
return nil
}
*err = handler(err)
}
func CopyFile(src, dst string) (err error) {
defer handle(&err, func(err error) error {
return fmt.Errorf("copy %s %s: %w", src, dst, err)
})
r, err := os.Open(src)
try err
defer r.Close()
w, err := os.Create(dst)
try err, func(err error) error {
os.Remove(dst) // only if Create fails
return fmt.Errorf("dir %s: %w", dst, err)
}
defer w.Close()
err = io.Copy(w, r)
try err
err = w.Close()
try err
return nil
}
引入 try-hanlder 后,能夠做到:
- 插入錯誤的返回語句,進(jìn)行機(jī)制預(yù)設(shè)。
- 在返回錯誤之前將錯誤處理函數(shù)組合在一起,便于后續(xù)的處理。
總結(jié)
在這個新提案中,一旦實(shí)施,就可以減少如下代碼的編寫:
if err != nil {
return ...
}
在代碼編寫上會節(jié)省一些行數(shù),且可以為錯誤處理機(jī)制引入一些新的 ”操作“,這是該提案的優(yōu)勢。
但是從 Go 開發(fā)者的角度而言,會引入一些新的副作用,例如:初學(xué)者的學(xué)習(xí)成本、Go 工具鏈的改造、程序理解的復(fù)雜度增加。
另外新的語句,似乎比較難與 Go1.13 引入的 error.Is 和 As 有較好的相關(guān)聯(lián)性。如果是做一個第三方用戶庫引入倒可以,但若是作為標(biāo)準(zhǔn)進(jìn)入 Go 源代碼中,似乎又有些格格不入(提案作者希望進(jìn)入)。
看了那么多提案,Go 錯誤處理機(jī)制的 ”升級“,似乎陷入了手心手背都是肉的階段...