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語言