Go defer 去掉閉包函數,靠譜嗎?
大家好,我是煎魚。
在 Go 語言里,defer 關鍵字是大家很愛用的。因為他有著 defer+recover+panic 的組合拳打法,還有種各種 defer close 等常用場景。
這是 Go 語言開發者必知必會的編程姿勢。
defer 常見用法
在語法上,Go defer 的代碼示例如下:
package main
import "fmt"
func main() {
defer fmt.Println("煎魚你好!")
fmt.Println("放學別走")
}
輸出結果:
放學別走
煎魚你好!
那 defer 在 Go 里的常見用法有哪些呢?首先是上文用到的,直接 defer + 函數:
defer f()
其次是 defer+閉包的方式:
defer func() {
result := f()
// do something with result
}()
其他還有在面試題上常被考究的傳參變形:
func f1() int {
i := 1
defer func() {
i++
}()
...
}
func f2() int {
i := 1
defer func(i int) {
i++
}(i)
....
}
這些代碼看起來,我們總是在對 defer 做閉包的各種聲明和使用。defer 會不會就是和閉包天生一對?
新提案:defer 代碼塊
最近大家也在討論一個與之相關的 Go 提案《proposal: Go 2: deferred code blocks[1]》,由 @Damien Lloyd 提出,想看看有沒有機會把 defer 的新語法落地。
圖片
原作者在使用 defer 時也是經常:
defer f()
但這樣就無法獲得返回值。最終要變成:
defer func() {
result := f()
// do something with result
}()
基于上述類似的原因,想引入如下具有 defer 作用的代碼塊語法:
defer {
// 在封閉函數的末尾執行此操作
}
在使用了 defer 關鍵字的函數最后執行這整個代碼塊 {...}。代碼塊中的每一行將按順序運行。
作者給出的代碼示例:
func fn() {
f, err := os.Create("eddycjy.txt")
if err != nil {
panic(err)
}
defer {
err := f.Close()
if err != nil {
panic(err)
}
}
}
在 fn 函數,聲明了 defer {...},代碼塊內是對 f.Close 的兜底判斷和異常拋出。在函數結束后執行這整個代碼塊。
反對的聲音
當然,這看著似乎是比較美好的。看起來原提案作者只是簡化了 defer 是的閉包使用,調整了作用域的范圍。
但在社區內其實遭受比較多的反對聲音。包含但不限于以下幾點:
1、收益比不高:這個提案只是避免了 func() 和 () 等閉包聲明,但是卻要增加新的 defer 語法(語言語法更改會帶來高昂成本),這個變更的 ROI 不高。
2、破壞兼容性:原 defer 關鍵字調用總是會跟著函數的詞法調用,有良好的一致性。如果進行修改,會產生新的隱晦,破壞一致性。也會對現有的許多工具(例如:靜態分析工具)產生影響,全要改。
3、作用域問題:原本 defer func{}() 的代碼塊結構下,你的代碼作用域都限于閉包函數下。而使用新的 defer {} 的結構,該返回和操作,是否會影響到外部函數的結果?(這是最有爭議的一點,作者也比較前言不搭后語,沒明確指明語法意思)
總結
一開始乍一眼一看,感覺只是把 defer 關鍵字語句簡化一下,好像特別好,省了幾個單詞。就像 if err != nil 也會有提要用 Rust 的 ? 等用法來替代的。
經過社區網友們指出后,發現這里貓膩不少。一門已經有 10+ 年的編程語言,還有 Go1 兼容性保障的。做出這類帶作用域的提案變更,是有比較大的風險的。
同時對于 Go 工具鏈的影響,也是非常大的。一改,直接都完犢子了。確實需要盡量深思。原作者完全沒提到。
該提案,我寫的時候正在開放 3 周等待意見收集。很神奇,沒更多的人說話,但提案的表情給了很多個不認同。