go中defer,panic,recover詳解 go中的異常處理
golang中defer,panic,recover是很常用的三個特性,三者一起使用可以充當其他語言中try…catch…的角色,而defer本身又像其他語言的解構函式
defer
在函式返回之前呼叫,用於資源釋放、列印日誌、異常捕獲等
f, err := os.Open(filename) if err != nil { return err } /** * 這裡defer要寫在err判斷的後邊而不是os.Open後邊 * 如果資源沒有獲取成功,就沒有必要對資源執行釋放操作 * 如果err不為nil而執行資源執行釋放操作,有可能導致panic */ defer f.Close()
如果有多個defer表示式,呼叫順序類似於棧,越後面的defer表示式越先被呼叫(後進先出)
func main() { defer fmt.Println(1) defer fmt.Println(2) defer fmt.Println(3) defer fmt.Println(4) }
結果:
如果含有 defer 表示式的函式有返回值,defer 表示式可能會在設定函式返回值之後,在返回到呼叫函式之前修改返回值,使最終的函式返回值與你想象的不一致,這裡很容易踩坑,來幾個:chestnut:
例1
func f() (result int) { defer func() { result++ }() return 0 }
例2
func f() (r int) { t := 5 defer func() { t = t + 5 }() return t }
例3
func f() (r int) { defer func(r int) { r = r + 5 }(r) return 1 }
請先不要向下看,在心裡跑一遍結果,然後去驗證
1的正確答案不是0,2的正確答案不是10,3的正確答案不是6
為什麼呢,最重要的一點就是要明白,return xxx這一條語句並不是一條原子指令
含有defer表示式的函式返回的過程是這樣的:先給返回值賦值,然後呼叫defer表示式,最後才是返回到呼叫函式中,可以用一個簡單的轉換規則將return xxx改寫成
返回值 = xxx 呼叫defer函式 空的return
例1,它可以改寫成這樣
func f() (result int) { //return語句不是一條原子呼叫,return xxx其實是賦值+ret指令 result = 0 //defer被插入到return之前執行,也就是賦返回值和ret指令之間 func() { result++ }() return }
所以這個返回值是1
例2,它可以改寫成這樣
func f() (r int) { t := 5 //賦值指令 r = t //defer被插入到賦值與返回之間執行,這個例子中返回值r沒被修改過 func() { t = t + 5 } return }
所以這個的結果是5
例3,它可以改寫成這樣
func f() (r int) { //給返回值賦值 r = 1 //這裡改的r是傳值傳進去的r(func裡邊的r是作用域不同的重名變數),不會改變要返回的那個r值 func(r int) { r = r + 5 }(r) return }
所以這個例子的結果是1
defer被插入到了賦值與ret之間,因此可能有機會改變最終的返回值
defer函式的引數值,是在申明defer時確定下來的
在defer函式申明時,對外部變數的引用是有兩種方式:作為函式引數和作為閉包引用
作為函式引數,在defer申明時就把值傳遞給defer,並將值快取起來,呼叫defer的時候使用快取的值進行計算
作為閉包引用,在defer函式執行時根據整個上下文確定當前的值
看個:chestnut:
func main() { i := 0 defer fmt.Println("a:", i) //閉包呼叫,將外部i傳到閉包中進行計算 defer func(j int) { fmt.Println("b:", j) }(i) //閉包呼叫,捕獲同作用域下的i進行計算 defer func() { fmt.Println("c:", i) }() i++ }
結果:
c: 1 b: 0 a: 0
說明地址:ofollow,noindex">https://www.jianshu.com/p/63e3d57f285f
panic和recover
func panic(v interface{})
中英文說明
The panic built-in function stops normal execution of the current goroutine.When a function F calls panic, normal execution of F stops immediately.Any functions whose execution was deferred by F are run in the usual way, and then F returns to its caller. To the caller G, the invocation of F then behaves like a call to panic,terminating G's execution and running any deferred functions.This continues until all functions in the executing goroutine have stopped,in reverse order. At that point, the program is terminated and the error condition is reported,including the value of the argument to panic. This termination sequence is called panicking and can be controlled by the built-in function recover.
panic內建函式停止當前goroutine的正常執行,當函式F呼叫panic時,函式F的正常執行被立即停止,然後執行所有在F函式中的defer函式,然後F返回到呼叫他的函式對於呼叫者G,F函式的行為就像panic一樣,終止G的執行並執行G中所defer函式,此過程會一直繼續執行到goroutine所有的函式。panic可以通過內建的recover來捕獲。
func recover() interface{}
中英文說明
The recover built-in function allows a program to manage behavior of a panicking goroutine. Executing a call to recover inside a deferred function (but not any function called by it) stops the panicking sequence by restoring normal execution and retrieves the error value passed to the call of panic. If recover is called outside the deferred function it will not stop a panicking sequence. In this case, or when the goroutine is not panicking, or if the argument supplied to panic was nil, recover returns nil. Thus the return value from recover reports whether the goroutine is panicking.
recover內建函式用來管理含有panic行為的goroutine,recover執行在defer函式中,獲取panic丟擲的錯誤值,並將程式恢復成正常執行的狀態。如果在defer函式之外呼叫recover,那麼recover不會停止並且捕獲panic錯誤如果goroutine中沒有panic或者捕獲的panic的值為nil,recover的返回值也是nil。由此可見,recover的返回值表示當前goroutine是否有panic行為
這裡有幾個需要注意的問題,通過:chestnut:表現
defer 表示式的函式如果定義在 panic 後面,該函式在 panic 後就無法被執行到
在defer前panic
func main() { panic("a") defer func() { fmt.Println("b") }() }
b沒有被打印出來,結果:
panic: a goroutine 1 [running]: main.main() /xxxxx/src/xxx.go:50 +0x39 exit status 2
在defer後panic
func main() { defer func() { fmt.Println("b") }() panic("a") }
b被正常列印,結果:
b panic: a goroutine 1 [running]: main.main() /xxxxx/src/xxx.go:50 +0x39 exit status 2
F中出現panic時,F函式會立刻終止,不會執行F函式中後面的內容,但不會立刻返回,而呼叫F的defer,如果F的defer中有recover捕獲,則F在執行完defer後正常返回,呼叫函式F的函式G繼續正常執行
func G() { defer func() { fmt.Println("c") }() F() fmt.Println("繼續執行") } func F() { defer func() { if err := recover(); err != nil { fmt.Println("捕獲異常:", err) } fmt.Println("b") }() panic("a") }
結果:
捕獲異常: a b 繼續執行 c
如果F的defer中無recover捕獲,則將panic拋到G中,G函式會立刻終止,不會執行G函式中後面的內容,但不會立刻返回,而呼叫G的defer...以此類推
func G() { defer func() { if err := recover(); err != nil { fmt.Println("捕獲異常:", err) } fmt.Println("c") }() F() fmt.Println("繼續執行") } func F() { defer func() { fmt.Println("b") }() panic("a") }
結果:
b 捕獲異常: a c
如果一直沒有recover,丟擲的panic到當前goroutine最上層函式時,程式直接異常終止
func G() { defer func() { fmt.Println("c") }() F() fmt.Println("繼續執行") } func F() { defer func() { fmt.Println("b") }() panic("a") }
結果:
b c panic: a goroutine 1 [running]: main.F() /xxxxx/src/xxx.go:61 +0x55 main.G() /xxxxx/src/xxx.go:53 +0x42 exit status 2
recover都是在當前的goroutine
裡進行捕獲的,這就是說,對於建立goroutine的外層函式,如果goroutine內部發生panic並且內部沒有用recover,外層函式是無法用recover來捕獲的,這樣會造成程式崩潰
func G() { defer func() { //goroutine外進行recover if err := recover(); err != nil { fmt.Println("捕獲異常:", err) } fmt.Println("c") }() //建立goroutine呼叫F函式 go F() time.Sleep(time.Second) } func F() { defer func() { fmt.Println("b") }() //goroutine內部丟擲panic panic("a") }
結果:
b panic: a goroutine 5 [running]: main.F() /xxxxx/src/xxx.go:67 +0x55 created by main.main /xxxxx/src/xxx.go:58 +0x51 exit status 2
recover返回的是interface{}型別而不是go中的 error 型別,如果外層函式需要呼叫err.Error(),會編譯錯誤,也可能會在執行時panic
func main() { defer func() { if err := recover(); err != nil { fmt.Println("捕獲異常:", err.Error()) } }() panic("a") }
編譯錯誤,結果:
err.Error undefined (type interface {} is interface with no methods)
func main() { defer func() { if err := recover(); err != nil { fmt.Println("捕獲異常:", fmt.Errorf("%v", err).Error()) } }() panic("a") }
結果:
捕獲異常: a
參考:
https: //golang.org/pkg/builtin/#recover
https://www.w3cschool.cn/go_internals/go_internals-drq7282o.html