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