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