Go defer的一些神奇規則,你瞭解嗎?

defer有一些規則,如果不瞭解,程式碼實現的最終結果會與預期不一致。對於這些規則,你瞭解嗎?

測試題

這是關於defer使用的程式碼,可以先考慮一下返回值。

package mainimport ( “fmt”)/** * @Author: Jason Pang * @Description: 快照 */func deferFuncParameter1() { var aInt = 1 defer fmt。Println(aInt) aInt = 2 return}/** * @Author: Jason Pang * @Description: 快照 */func deferFuncParameter2() { var aInt = 1 defer func(t int) { fmt。Println(t) }(aInt) aInt = 2 return}/** * @Author: Jason Pang * @Description: 動態 */func deferFuncParameter3() { var aInt = 1 defer func() { fmt。Println(aInt) }() aInt = 2 return}/** * @Author: Jason Pang * @Description: 影響返回值 * @return ret */func deferFuncReturn1() (ret int) { ret = 10 defer func() { ret++ fmt。Println(“——-”, ret) }() return 2}/** * @Author: Jason Pang * @Description: 不影響返回值 * @return ret */func deferFuncReturn2() (ret int) { ret = 10 defer func(ret int) { ret++ fmt。Println(“——-”, ret) }(ret) return 2}/** * @Author: Jason Pang * @Description: defer順序 */func deferFuncSeq1() { var aInt = 1 defer fmt。Println(aInt) aInt = 2 defer fmt。Println(aInt) return}func main() { fmt。Println(“快照”) deferFuncParameter1() deferFuncParameter2() deferFuncParameter3() fmt。Println(“返回值”) fmt。Println(deferFuncReturn1()) fmt。Println(deferFuncReturn2()) fmt。Println(“執行順序”) deferFuncSeq1()}

正確輸出為:

➜ myproject go run main。go

快照

1

1

2

返回值

——- 3

3

——- 11

2

執行順序

2

1

分析

defer有幾條重要規則,上面的結果都能從這些規則中找到答案。

規則一 當defer被宣告時,其引數就會被實時解析

當defer被宣告的時候,如果直接使用了引數,

此時的引數就會使用快照值

,在整個生命週期內不會變化。如deferFuncParameter1、deferFuncParameter2,雖然aInt在defer聲明後被變更,但defer裡的值不會再變了。

func deferFuncParameter1() { var aInt = 1 defer fmt。Println(aInt) aInt = 2 return}func deferFuncParameter2() { var aInt = 1 defer func(t int) { fmt。Println(t) }(aInt) aInt = 2 return}

與之相反的是deferFuncParameter3,隨aInt的變化而變化。

func deferFuncParameter3() { var aInt = 1 defer func() { fmt。Println(aInt) }() aInt = 2 return}

規則二 defer可能操作主函式的具名返回值

defer有可能更改函式的返回值,這是最容易導致錯誤的地方。

關鍵字

return

不是一個原子操作,實際上

return

只代理彙編指令

ret

,即將跳轉程式執行。比如語句 return i ,實際上分兩步進行,即將i值存入棧中作為返回值,然後執行跳轉,而defer的執行時機正是跳轉前,所以說defer執行時還是有機會操作返回值的。return i的執行過程如下所示:

result = i 執行deferreturn

所以基於這個規則,對於deferFuncReturn1,

func deferFuncReturn1() (ret int) { ret = 10 defer func() { ret++ fmt。Println(“——-”, ret) }() return 2}

執行過程為:

ret = 2ret++fmt。Println(“——-”, ret)return

所以最終ret的值為3。

對於deferFuncReturn2,因為defer宣告的時候直接使用了引數,所以使用的是快照,不會影響ret的返回值。

規則三 延遲函式執行按後進先出順序執行,即先出現的 defer最後執行

這個規則大家都很熟悉,defer按照棧的順序執行。

坑例項

舉一個錯誤使用defer的例項。在go中使用事務時,有一種推薦寫法:將Rollback放到defer中,透過判斷函式是否有報錯或者panic,來判斷是否要回滾。

func Update() (resp *baseinfo。Resp, err error) { //開啟事務 panicked := true tx, err := db。TXBegin() if err != nil { return resp, nil } defer func() { if panicked || err != nil { tx。Rollback() } }() //更新 err = h。update(shopId, tx) if err != nil {//失敗返回 return resp, nil } panicked = false err = tx。Commit()。Error if err != nil { //失敗返回 return resp, nil } return}

判斷回滾的err正是函式的具名返回值,在有報錯的情況下,返回值被賦值為nil,這意味如果有失敗,Rollback也不會被執行。

之所以不將err直接返回,而是使用nil,是因為框架設計的問題,業務錯誤透過resp返回,如果直接返回err,框架會認為是RPC錯誤。

總結

對每一個知識點,都需要有準確的瞭解,尤其在本有機會去了解的時候,否則極易寫出問題。

資料

golang中defer的使用規則

Go專家程式設計

最後

大家如果喜歡我的文章,可以關注我的公眾號(程式設計師麻辣燙)

我的個人部落格為:https://shidawuhen。github。io/

Go defer的一些神奇規則,你瞭解嗎?

往期文章回顧:

設計模式

招聘

思考

儲存

算法系列

讀書筆記

小工具

架構

網路

Go語言