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

語言: CN / TW / HK

theme: channing-cyan

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

測試題

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

```go package main

import ( "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裏的值不會再變了。

```go 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的變化而變化。

```go 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的執行過程如下所示:

```go result = i 執行defer return

```

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

```go func deferFuncReturn1() (ret int) { ret = 10 defer func() { ret++ fmt.Println("-----", ret) }() return 2 }

```

執行過程為:

```go ret = 2 ret++ fmt.Println("-----", ret) return

```

所以最終ret的值為3。

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

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

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

坑實例

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

```go 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錯誤。

總結

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

資料

  1. golang中defer的使用規則

  2. Go專家編程

最後

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

我的個人博客為:https://shidawuhen.github.io/

往期文章回顧:

  1. 設計模式

  2. 招聘

  3. 思考

  4. 存儲

  5. 算法系列

  6. 讀書筆記

  7. 小工具

  8. 架構

  9. 網絡

  10. Go語言