Golang 高效實踐之defer、panic、recover實踐
前言
我們知道Golang處理異常是用error返回的方式,然後呼叫方根據error的值走不同的處理邏輯。但是,如果程式觸發其他的嚴重異常,比如說陣列越界,程式就要直接崩潰。Golang有沒有一種異常捕獲和恢復機制呢?這個就是本文要講的panic和recover。其中recover要配合defer使用才能發揮出效果。
Defer
Defer語句將一個函式放入一個列表(用棧表示其實更準確)中,該列表的函式在環繞defer的函式返回時會被執行。defer通常用於簡化函式的各種各樣清理動作,例如關閉檔案,解鎖等等的釋放資源的動作。例如下面的這個函式開啟兩個檔案,從一個檔案拷貝內容到另外的一個檔案:
func CopyFile(dstName, srcName string) (written int64, err error) { src, err := os.Open(srcName) if err != nil { return } dst, err := os.Create(dstName) if err != nil { return } written, err = io.Copy(dst, src) dst.Close() src.Close() return }
這段程式碼可以工作,但是有一個bug。如果呼叫os.Create失敗,函式將會直接返回,並沒有關閉srcName檔案。修復的方法很簡單,可以把src.Close的呼叫放在第二個return語句前面。但是當我們程式的分支比較多的時候,也就是說當該函式還有幾個其他的return語句時,就需要在每個分支return前都要加上close動作。這樣使得資源的清理非常繁瑣而且容易遺漏。所以Golang引入了defer語句:
func CopyFile(dstName, srcName string) (written int64, err error) { src, err := os.Open(srcName) if err != nil { return } defer src.Close() dst, err := os.Create(dstName) if err != nil { return } defer dst.Close() return io.Copy(dst, src) }
在每個資源申請成功的後面都加上defer自動清理,不管該函式都多少個return,資源都會被正確的釋放,例如上述例子的檔案一定會被關閉。
關閉defer語句,有三條簡單的規則:
1.defer的函式在壓棧的時候也會儲存引數的值,並非在執行時取值。
func a() { i := 0 defer fmt.Println(i) i++ return }
例如該示例中,變數i會在defer時就被儲存起來,所以defer函式執行時i的值是0.即便後面i的值變為了1,也不會影響之前的拷貝。
2.defer函式呼叫的順序是後進先出。
func b() { for i := 0; i < 4; i++ { defer fmt.Print(i) } }
函式輸出3210
3.defer函式可以讀取和重新賦值函式的命名返回引數。
func c() (i int) { defer func() { i++ }() return 1 }
這個例子中,defer函式中在函式返回時對命名返回值i進行了加1操作,因此函式返回值是2.可能你會有疑問,規則1不是說會在defer時儲存i的值嗎?儲存的i是0,那加1操作之後也是1啊。這裡就是閉包的魅力,i的值會被立馬儲存,但是儲存的是i的引用,也可以理解為指標。當實際執行加1操作時,i的值其實被return置為了1,defer執行了加1操作i的值也就變成了2.
Panic
Panic是內建的停止控制流的函式。相當於其他程式語言的拋異常操作。當函式F呼叫了panic,F的執行會被停止,在F中panic前面定義的defer操作都會被執行,然後F函式返回。對於呼叫者來說,呼叫F的行為就像呼叫panic(如果F函式內部沒有把panic recover掉)。如果都沒有捕獲該panic,相當於一層層panic,程式將會crash。panic可以直接呼叫,也可以是程式執行時錯誤導致,例如陣列越界。
Recover
Recover是一個從panic恢復的內建函式。Recover只有在defer的函式裡面才能發揮真正的作用。如果是正常的情況(沒有發生panic),呼叫recover將會返回nil並且沒有任何影響。如果當前的goroutine panic了,recover的呼叫將會捕獲到panic的值,並且恢復正常執行。
例如下面這個例子:
package main import "fmt" func main() { f() fmt.Println("Returned normally from f.") } func f() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered in f", r) } }() fmt.Println("Calling g.") g(0) fmt.Println("Returned normally from g.") } func g(i int) { if i > 3 { fmt.Println("Panicking!") panic(fmt.Sprintf("%v", i)) } defer fmt.Println("Defer in g", i) fmt.Println("Printing in g", i) g(i + 1) }
函式g接受引數i,如果i大於3時觸發panic,否則對i進行加1操作。函式f的defer函式裡面呼叫了recover並且列印recover的值(非nil的話)。
程式將會輸出:
Calling g. Printing in g 0 Printing in g 1 Printing in g 2 Printing in g 3 Panicking! Defer in g 3 Defer in g 2 Defer in g 1 Defer in g 0 Recovered in f 4 Returned normally from f.
Panic和recover可以接受任何型別的值,因為定義為interface{}:
func panic(v interface{})
func recover() interface{}
所以工作模式相當於:
panic(value)->recover()->value
傳遞給panic的value最終由recover捕獲。
另外defer可以配合鎖的使用來確保鎖的釋放,例如:
mu.Lock()
Defer mu.Unlock()
需要注意的是這樣會延長鎖的釋放時間(需要等到函式return)。
容易踩坑的一些例子
通過上面的說明,我們已經對defer,panic和recover有了比較清晰的認識,下面通過一些實戰中容易踩坑的例子來加深下印象。
在迴圈裡面使用defer
不要在迴圈裡面使用defer,除非你真的確定defer的工作流程,例如:
只有當函式返回時defer的函式才會被執行,如果在for迴圈裡面defer定義的函式會不斷的壓棧,可能會爆棧而導致程式異常。
解決方法1:將defer移動到迴圈之外
解決方法2:構造一層新的函式包裹defer
defer方法
沒有指標的情況:
type Car struct { model string } func (c Car) PrintModel() { fmt.Println(c.model) } func main() { c := Car{model: "DeLorean DMC-12"} defer c.PrintModel() c.model = "Chevrolet Impala" }
程式輸出DeLorean DMC-12。根據我們前面講的內容,defer的時候會把函式和參考拷貝一份儲存起來,所以c.model的值後面改變也不會影響defer的執行。
有指標的情況:
Car PrintModel()方法定義改為:
func (c *Car) PrintModel() { fmt.Println(c.model) }
程式將會輸出Chevrolet Impala。這些defer雖然將函式和引數儲存了起來,但是由於引數的值本身是針對,隨意後面的改動會影響到defer函式的行為。
同理的例子還有:
for i := 0; i < 3; i++ { defer func() { fmt.Println(i) }() }
程式將會輸出:
3 3 3
因為閉包引用匿名函式外面的變數相當於是指標引用,得到的是變數的地址,實際到defer真正執行時,指標指向的內容已經發生的變化:
解決的方法:
for i := 0; i < 3; i++ { defer func(i int) { fmt.Println(i) }(i) }
或者:
for i := 0; i < 3; i++ { defer fmt.Println(i) }
程式輸出:
2 1 0
這裡就不會用到閉包的上下文引用特性,是正經的函式引數拷貝傳遞,所以不會有問題。
defer中修改函式error返回值
package main import ( "errors" "fmt" ) func main() { { err := release() fmt.Println(err) } { err := correctRelease() fmt.Println(err) } } func release() error { defer func() error { return errors.New("error") }() return nil } func correctRelease() (err error) { defer func() { err = errors.New("error") }() return nil }
release函式中error的值並不會被defer的return返回,因為匿名返回值在defer執行前就已經宣告好並複製為nil。correctRelease函式能夠修改返回值是因為閉包的特性,defer中的err是實際的返回值err地址引用,指向的是同一個變數。defer修改程式返回值error一般用在和recover搭配中,上述的情況屬於濫用defer的一種情況,其實error函式值可以直接在程式的return中修改,不用defer。
總結
文章介紹了defer、panic和recover的原理和用法,並且在最後給出了一些在實際應用的實踐建議,不要濫用defer,注意defer搭配閉包時的一些特性。
參考
https://blog.golang.org/defer-panic-and-recover
https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1-8d070894cb01
https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-ii-cc550f6ad9aa
https://blog.learngoprogramming.com/golang-defer-simplified-77d3b2b817ff
https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-iii-36a1ab3d6ef1