Go 学习笔记(19)— 函数(05)[如何触发 panic、触发 panic 延迟执行、panic 和 recover 的关系]
1. 異常設計思想
Go 語言的錯誤處理思想及設計包含以下特征:
-
一個可能造成錯誤的函數,需要返回值中返回一個錯誤接口(
error),如果調用是成功的,錯誤接口將返回nil,否則返回錯誤。 -
在函數調用后需要檢查錯誤,如果發生錯誤,則進行必要的錯誤處理。
Go 里沒有用經典的 try/except 捕獲異常。Go 提供兩種錯誤處理方式
- 函數返回
error類型對象判斷錯誤 panic異常
一般而言,當宕機發生時,程序會中斷運行,并立即執行在該 goroutine (可以先理解成線程)中被延遲的函數( defer 機制),隨后,程序崩潰并輸出日志信息,日志信息包括 panic value 和函數調用的堆棧跟蹤信息, panic value 通常是某種錯誤信息。
雖然 Go 語言的 panic 機制類似于其他語言的異常,但 panic 的適用場景有一些不同,由于 panic 會引起程序的崩潰,因此 panic 一般用于嚴重錯誤,如程序內部的邏輯不一致。
任何崩潰都表明了我們的代碼中可能存在漏洞,所以對于大部分漏洞,我們應該使用 Go 語言提供的錯誤機制,而不是 panic 。
一般情況下在 Go 里只使用 error 類型判斷錯誤, Go 官方希望開發者能夠很清楚的掌控所有的異常,在每一個可能出現異常的地方都返回或判斷 error 是否存在。
panic可以手工調用,但是 Go 官方建議盡量不要使用panic,每一個異常都應該用 error 對象捕獲。
如果異常出現了,但沒有被捕獲并恢復,Go 程序的執行就會被終止,即便出現異常的位置不在主 Goroutine 中也會這樣。
2. 如何觸發 panic
使用 panic 拋出異常后,函數執行將從調用 panic 的地方停止,如果函數內有 defer 調用,則執行 defer 后邊的函數調用,如果 defer 調用的函數中沒有捕獲異常信息,這個異常會沿著函數調用棧往上傳遞,直到 main 函數仍然沒有捕獲異常,將會導致程序異常退出。示例代碼:
package main
func demo() {panic("拋出異常")
}
func main() {demo()
}
package mainimport ("fmt"
)func main() {panic("crash")fmt.Println("end")}
輸出結果:
panic: crashgoroutine 1 [running]:
main.main()/home/wohu/gocode/src/hello.go:8 +0x39
exit status 2
以上代碼中只用了一個內建的函數 panic() 就可以造成崩潰, panic() 的聲明如下:
func panic(v interface{}) //panic() 的參數可以是任意類型的。
請謹慎使用panic 函數拋出異常,如果沒有捕獲異常,將會導致程序異常退出。
3. 觸發 panic 延遲執行
在 Go 中,panic 主要有兩類來源,一類是來自 Go 運行時,另一類則是 Go 開發人員通過 panic 函數主動觸發的。
當 panic() 觸發的宕機發生時, panic() 后面的代碼將不會被運行,但是在 panic() 函數前面已經運行過的 defer 語句依然會在宕機發生時發生作用,參考下面代碼:
package mainimport ("fmt"
)func main() {defer fmt.Println("defef run")panic("crash")fmt.Println("end")}
輸出結果:
defef run
panic: crashgoroutine 1 [running]:
main.main()/home/wohu/gocode/src/hello.go:10 +0x95
exit status 2
從結果中可以看到,觸發 panic 前, defer 語句會被優先執行。
panic() 是一個內建函數,可以中斷原有的控制流程,進入一個令人 panic 的流程中。當函數 main 調用 panic,函數的執行被中斷,但是 main 中的延遲函數(必須是在 panic 之前的已加載的 defer )會正常執行,然后 main 返回到調用它的地方。在調用的地方,main 的行為就像調用了 panic。這一過程繼續向上,直到發生 panic 的 goroutine 中所有調用的函數返回,此時程序退出。
異常可以直接調用 panic 產生。也可以由運行時錯誤產生,例如訪問越界的數組。
4. recover 使用
recover 是一個 Go 語言的內建函數,可以讓進入宕機流程中的 goroutine 恢復過來。
recover 僅在延遲函數 defer 中有效:
-
在正常的執行過程中,調用
recover會返回nil并且沒有其他任何效果; -
如果當前的
goroutine陷入panic,調用recover可以捕獲到panic的輸入值,并且恢復正常的執行;
注意:
在其他語言里,
panic往往以異常的形式存在,底層拋出異常,上層邏輯通過try/catch機制捕獲異常,沒有被捕獲的嚴重異常會導致宕機,捕獲的異常可以被忽略,讓代碼繼續運行。
Go 語言沒有異常系統,其使用 panic 觸發宕機類似于其他語言的拋出異常, recover 的宕機恢復機制就對應其它語言中的 try/catch 機制。
package mainfunc test() {defer func() {if err := recover(); err != nil { // recover 捕獲錯誤。println(err.(string)) // 將 interface{} 轉型為具體類型。}}()panic("panic error!") // panic 拋出錯誤
}
func main() {test()
}
由于 panic 、 recover 參數類型為 interface{} ,因此可拋出任何類型對象。
func panic(v interface{})
func recover() interface{}
延遲調用中引發的錯誤,可被后續延遲調用捕獲,但僅最后一個錯誤可被捕獲。
package mainimport "fmt"func test() {defer func() {fmt.Println(recover())}()defer func() {panic("defer panic")}()panic("test panic")
}
func main() {test()
}
輸出:
defer panic
捕獲函數 recover 只有在延遲調用內直接調用才會終止錯誤,否則總是返回 nil 。任何未捕獲的錯誤都會沿調用堆棧向外傳遞。
當沒有異常信息拋出時, recover 函數返回值是 nil 。 recover 只有在 defer 調用的函數內部時,才能阻止 panic 拋出的異常信息繼續向上傳遞,如果不是在 defer 調用的函數內部,將會失效。
package mainimport "fmt"func test() {defer recover() // 無效!defer fmt.Println(recover()) // 無效!defer func() {func() {println("defer inner")recover() // 無效!}()}()panic("test panic")
}
func main() {test()
}
輸出
defer inner
<nil>
panic: test panic
使用延遲匿名函數或下面這樣都是有效的。
package mainimport "fmt"func except() {fmt.Println(recover())
}
func test() {defer except()panic("test panic")
}
func main() {test()
}
如果需要保護代碼片段,可將代碼塊重構成匿名函數,如此可確保后續代碼被執行。
package mainimport "fmt"func test(x, y int) {var z intfunc() {defer func() {err := recover()fmt.Println(err)if err != nil {z = 0}}()z = x / yreturn}()println("x / y =", z)
}
func main() {test(10, 0)
}
輸出結果:
runtime error: integer divide by zero
x / y = 0
recover 的正確用法:
package mainimport ("errors""fmt"
)func main() {fmt.Println("Enter function main.")defer func() {fmt.Println("Enter defer function.")// recover函數的正確用法。if p := recover(); p != nil {fmt.Printf("panic: %s\n", p)}fmt.Println("Exit defer function.")}()// recover函數的錯誤用法。fmt.Printf("no panic: %v\n", recover())// 引發panic。panic(errors.New("something wrong"))// recover函數的錯誤用法。p := recover()fmt.Printf("panic: %s\n", p)fmt.Println("Exit function main.")
}
5. panic 和 recover 的關系
如何區別使用 panic 和 error 兩種方式?
慣例是:導致關鍵流程出現不可修復性錯誤的使用 panic ,其他使用 error 。
panic 和 recover 的組合有如下特性:
- 有
panic沒recover,程序宕機。 - 有
panic也有recover,程序不會宕機,執行完對應的defer后,從宕機點退出當前函數后繼續執行。
注意:
雖然 panic/recover 能模擬其他語言的異常機制,但并不建議在編寫普通函數時也經常性使用這種特性。
在 panic 觸發的 defer 函數內,可以繼續調用 panic ,進一步將錯誤外拋,直到程序整體崩潰。
如果想在捕獲錯誤時設置當前函數的返回值,可以對返回值使用命名返回值方式直接進行設置。
6. 實際項目使用
Go 并發編程中,每一個 goroutine 出現 panic,都會讓整個進程退出,如果能夠捕獲異常,那么出現 panic 的時候,整個服務不會掛掉,只是當前導致 panic 的某個 goroutine 會出現異常,通過捕獲異常可以繼續執行任務,建議還是在某些有必要的條件和入口處進行異常捕獲。
常見拋出異常的情況:數組越界、空指針空對象,類型斷言失敗等。
package mainimport ("fmt""time"
)// 拋出異常,模擬實際 Panic 的場景
func throwException() {panic("An exception is thrown! Start Panic")
}// Go 的 defer + recover 來捕獲異常
func catchExceptions() {defer func() {if e := recover(); e != nil {fmt.Printf("Panicing %s\n", e)}}()go func() {// 做具體的實現任務fmt.Print("do something \n")}()throwException()fmt.Printf("Catched an exceptions\n")
}func main() {fmt.Printf("==== start main =====\n")// 執行一次catchExceptions()num := 1for {num++fmt.Printf("\nstart circle num:%v\n", num)// 循環執行,如果實際項目中,這個函數是主任務的話,需要一個 for 來循環執行,避免捕獲一次 Panic 之后就不再繼續執行catchExceptions()time.Sleep(3 * time.Second)if num == 5 {fmt.Printf("==== end main =====\r\n")return}}
}
一般的建議是在請求來源入口處的函數或者關鍵路徑上實現這么一段代碼進行捕獲,這樣,只要通過這個入口出現的異常都能被捕獲,并打印詳細日志。同時,為了保證 goroutine 能夠繼續執行任務,因此還要考慮當出現 panic 被捕獲之后,是否有主動循環或者被動觸發來重新執行任務。
7. 如何應對 panic
7.1 評估程序對 panic 的忍受度
Go 標準庫提供的 http server 采用的是,每個客戶端連接都使用一個單獨的 Goroutine 進行處理的并發處理模型。也就是說,客戶端一旦與 http server 連接成功,http server 就會為這個連接新創建一個 Goroutine,并在這 Goroutine 中執行對應連接(conn)的 serve 方法,來處理這條連接上的客戶端請求。
無論在哪個 Goroutine 中發生未被恢復的 panic,整個程序都將崩潰退出。所以,為了保證處理某一個客戶端連接的 Goroutine 出現 panic 時,不影響到 http server 主 Goroutine 的運行,Go 標準庫在 serve 方法中加入了對 panic 的捕捉與恢復,下面是 serve 方法的部分代碼片段:
// $GOROOT/src/net/http/server.go
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {c.remoteAddr = c.rwc.RemoteAddr().String()ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())defer func() {if err := recover(); err != nil && err != ErrAbortHandler {const size = 64 << 10buf := make([]byte, size)buf = buf[:runtime.Stack(buf, false)]c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)}if !c.hijacked() {c.close()c.setState(c.rwc, StateClosed, runHooks)}}()... ...
}
你可以看到,serve 方法在一開始處就設置了 defer 函數,并在該函數中捕捉并恢復了可能出現的 panic。這樣,即便處理某個客戶端連接的 Goroutine 出現 panic,處理其他連接 Goroutine 以及 http server 自身都不會受到影響。
這種局部不要影響整體的異常處理策略,在很多并發程序中都有應用。并且,捕捉和恢復 panic 的位置通常都在子 Goroutine 的起始處,這樣設置可以捕捉到后面代碼中可能出現的所有 panic,就像 serve 方法中那樣。
7.2 提示潛在 bug
在 json 包的 encode.go 中也有使用 panic 做潛在 bug 提示的例子:
// $GOROOT/src/encoding/json/encode.go
func (w *reflectWithString) resolve() error {... ...switch w.k.Kind() {case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:w.ks = strconv.FormatInt(w.k.Int(), 10)return nilcase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:w.ks = strconv.FormatUint(w.k.Uint(), 10)return nil}panic("unexpected map key type")
}
這段代碼中,resolve 方法的最后一行代碼就相當于一個“代碼邏輯不會走到這里”的斷言。一旦觸發“斷言”,這很可能就是一個潛在 bug。
我們也看到,去掉這行代碼并不會對 resolve 方法的邏輯造成任何影響,但真正出現問題時,開發人員就缺少了“斷言”潛在 bug 提醒的輔助支持了。在 Go 標準庫中,大多數 panic 的使用都是充當類似斷言的作用的。
總結
以上是生活随笔為你收集整理的Go 学习笔记(19)— 函数(05)[如何触发 panic、触发 panic 延迟执行、panic 和 recover 的关系]的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 以男女的审美,刘诗诗、赵薇、林心如、刘亦
- 下一篇: 欧体字是谁写的啊?