Go?panic的三種產(chǎn)生方式細(xì)節(jié)探究
為什么 panic 值得思考?
初學(xué) Go 的時(shí)候,心里常常很多疑問,有時(shí)候看似懂了的問題,其實(shí)是是而非。
panic 究竟是啥?看似顯而易見的問題,但是卻回答不出個(gè)所以然來(lái)。奇伢分兩個(gè)章節(jié)來(lái)徹底搞懂 panic 的知識(shí):
- 姿勢(shì)篇:摸清楚 panic 的誕生,它不是石頭里蹦出來(lái)的,總結(jié)有三種姿勢(shì);
- 原理篇:徹底搞明白 panic 的內(nèi)部原理,理解 panic 的深層原理;
panic 的三種姿勢(shì)
什么時(shí)候會(huì)產(chǎn)生 panic ?
我們先從“形”來(lái)學(xué)習(xí)。從程序猿的角度來(lái)看,可以分為主動(dòng)和被動(dòng)方式,被動(dòng)的方式有兩種,如下:
主動(dòng)方式:
- 程序猿主動(dòng)調(diào)用
panic( )
函數(shù);
被動(dòng)的方式:
- 編譯器的隱藏代碼觸發(fā);
- 內(nèi)核發(fā)送給進(jìn)程信號(hào)觸發(fā) ;
編譯器的隱藏代碼
Go 之所以簡(jiǎn)單又強(qiáng)大,編譯器居功至偉。非常多的事情是編譯器幫程序猿做了的,邏輯補(bǔ)充,內(nèi)存的逃逸分析等等。
包括 panic 的拋出!
舉個(gè)非常典型的例子:整數(shù)算法除零會(huì)發(fā)生 panic,怎么做到的?
看一段極簡(jiǎn)代碼:
func divzero(a, b int) int { c := a/b return c }
上面函數(shù)就會(huì)有除零的風(fēng)險(xiǎn),當(dāng) b 等于 0 的時(shí)候,程序就會(huì)觸發(fā) panic,然后退出,如下:
root@ubuntu:~/code/gopher/src/panic# ./test_zero panic: runtime error: integer divide by zero goroutine 1 [running]: main.zero(0x64, 0x0, 0x0) /root/code/gopher/src/panic/test_zero.go:6 +0x52
問題來(lái)了:程序怎么觸發(fā)的 panic ?
代碼面前無(wú)秘密。
可代碼看不出啥呀,不就是一行 c := a/b
嘛?
奇伢說的是匯編代碼。因?yàn)檫@段隱藏起來(lái)的邏輯,是編譯器幫你加的。
用 dlv 調(diào)試斷點(diǎn)到 divzero
函數(shù),然后執(zhí)行 disassemble
,你就能看到秘密了。奇伢截取部分匯編,并備注了下:
(dlv) disassemble TEXT main.zero(SB) /root/code/gopher/src/panic/test_zero.go // 判斷 b 是否等于 0 test_zero.go:6 0x4aa3c1 4885c9 test rcx, rcx // 不等于 0 就跳轉(zhuǎn)到 0x4aa3c8 執(zhí)行指令,否則就往下執(zhí)行 test_zero.go:6 0x4aa3c4 7502 jnz 0x4aa3c8 // 執(zhí)行到這里,就說明 b 是 0 值,就跳轉(zhuǎn)到 0x4aa3ed ,也就是 call $runtime.panicdivide => test_zero.go:6 0x4aa3c6 eb25 jmp 0x4aa3ed test_zero.go:6 0x4aa3c8 4883f9ff cmp rcx, -0x1 test_zero.go:6 0x4aa3cc 7407 jz 0x4aa3d5 test_zero.go:6 0x4aa3ce 4899 cqo test_zero.go:6 0x4aa3d0 48f7f9 idiv rcx // ... test_zero.go:7 0x4aa3ec c3 ret // 看到神奇的函數(shù)了嘛 ! test_zero.go:6 0x4aa3ed e8ee27f8ff call $runtime.panicdivide
看到秘密的函數(shù)了嗎?
編譯器偷偷加上了一段 if/else
的判斷邏輯,并且還給加了 runtime.panicdivide
的代碼。
- 如果 b == 0 ,那么跳轉(zhuǎn)執(zhí)行函數(shù)
runtime.panicdivide
;
再來(lái)看一眼 panicdivide
函數(shù),這是一段極簡(jiǎn)的封裝:
// runtime/panic.go func panicdivide() { panicCheck2("integer divide by zero") panic(divideError) }
看到了不,這里面調(diào)用的就是 panic()
函數(shù)。
除零觸發(fā)的 panic 就是這樣來(lái)的,它不是石頭里蹦出來(lái)的,而是編譯器多加的邏輯判斷保證了除數(shù)為 0 的時(shí)候,觸發(fā) panic 函數(shù)。
劃重點(diǎn):編譯器加的隱藏邏輯,調(diào)用了拋出 panic 的函數(shù)。Go 的編譯器才是真大佬!
進(jìn)程信號(hào)觸發(fā)
最典型的是非法地址訪問,比如, nil 指針 訪問會(huì)觸發(fā) panic,怎么做到的?
看一個(gè)極簡(jiǎn)的例子:
func nilptr(b *int) int { c := *b return c }
當(dāng)調(diào)用 nilptr( nil )
的時(shí)候,將會(huì)導(dǎo)致進(jìn)程異常退出:
root@ubuntu:~/code/gopher/src/panic# ./test_nil panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4aa3bc] goroutine 1 [running]: main.nilptr(0x0, 0x0) /root/code/gopher/src/panic/test_nil.go:6 +0x1c
問題來(lái)了:這里的 panic 又是怎么形成的呢?
在 Go 進(jìn)程啟動(dòng)的時(shí)候會(huì)注冊(cè)默認(rèn)的信號(hào)處理程序( sigtramp
)
在 cpu 訪問到 0 地址會(huì)觸發(fā) page fault 異常,這是一個(gè)非法地址,內(nèi)核會(huì)發(fā)送 SIGSEGV
信號(hào)給進(jìn)程,所以當(dāng)收到 SIGSEGV
信號(hào)的時(shí)候,就會(huì)讓 sigtramp
函數(shù)來(lái)處理,最終調(diào)用到 panic
函數(shù) :
// 信號(hào)處理函數(shù)回調(diào) sigtramp (純匯編代碼) -> sigtrampgo ( signal_unix.go ) -> sighandler ( signal_sighandler.go ) -> preparePanic ( signal_amd64x.go ) -> sigpanic ( signal_unix.go ) -> panicmem -> panic (內(nèi)存段錯(cuò)誤)
在 sigpanic
函數(shù)中會(huì)調(diào)用到 panicmem
,在這個(gè)里面就會(huì)調(diào)用 panic 函數(shù),從而走上了 Go 自己的 panic 之路。
panicmem
和 panicdivide
類似,都是對(duì) panic( )
的極簡(jiǎn)封裝:
func panicmem() { panicCheck2("invalid memory address or nil pointer dereference") panic(memoryError) }
劃重點(diǎn):這種方式是通過信號(hào)軟中斷的方式來(lái)走到 Go 注冊(cè)的信號(hào)處理邏輯,從而調(diào)用到 panic( )
的函數(shù)。
童鞋可能會(huì)好奇,信號(hào)處理的邏輯什么時(shí)候注冊(cè)進(jìn)去的?
在進(jìn)程初始化的時(shí)候,創(chuàng)建 M0(線程)的時(shí)候用系統(tǒng)調(diào)用 sigaction
給信號(hào)注冊(cè)處理函數(shù)為 sigtramp
,調(diào)用棧如下:
mstartm0 (proc.go) -> initsig (signal_unix.go:113) -> setsig (os_linux.go)
這樣的話,以后觸發(fā)了信號(hào)軟中斷,就能調(diào)用到 Go 的信號(hào)處理函數(shù),從而進(jìn)行語(yǔ)言層面的 panic 處理 。
總的來(lái)說,這個(gè)是從系統(tǒng)層面到特定語(yǔ)言層面的處理轉(zhuǎn)變。
程序猿主動(dòng)
第三種方式,就是程序猿自己主動(dòng)調(diào)用 panic
拋出來(lái)的。
func main() { panic("panic test") }
簡(jiǎn)單的函數(shù)調(diào)用,這個(gè)超簡(jiǎn)單的。
聊聊 panic 到底是什么?
現(xiàn)在我們摸透了 panic 產(chǎn)生的姿勢(shì),以上三種方式,無(wú)論哪一種都?xì)w一到 panic( )
這個(gè)函數(shù)調(diào)用。所以有一點(diǎn)很明確:panic 這個(gè)東西是語(yǔ)言層面的處理邏輯。
panic 發(fā)生之后,如果 Go 不做任何特殊處理,默認(rèn)行為是打印堆棧,退出程序。
現(xiàn)在回到最本源的問題:panic 到底是什么?
這里不糾結(jié)概念,只描述幾個(gè)簡(jiǎn)單的事實(shí):
panic( )
函數(shù)內(nèi)部會(huì)產(chǎn)生一個(gè)關(guān)鍵的數(shù)據(jù)結(jié)構(gòu)體_panic
,并且掛接到 goroutine 之上;panic( )
函數(shù)內(nèi)部會(huì)執(zhí)行_defer
函數(shù)鏈條,并針對(duì)_panic
的狀態(tài)進(jìn)行對(duì)應(yīng)的處理;
什么叫做 panic( )
的對(duì)應(yīng)的處理?
循環(huán)執(zhí)行 goroutine 上面的 _defer
函數(shù)鏈,如果執(zhí)行完了都還沒有恢復(fù) _panic
的狀態(tài),那就沒得辦法了,退出進(jìn)程,打印堆棧。
如果在 goroutine 的 _defer
鏈上,有個(gè)朋友 recover 了一下,把這個(gè) _panic
標(biāo)記成恢復(fù),那事情就到此為止,就從這個(gè) _defer
函數(shù)執(zhí)行后續(xù)正常代碼即可,走 deferreturn
的邏輯。
所以,panic 是什么 ?
小奇伢認(rèn)為,它就是個(gè)特殊函數(shù)調(diào)用,僅此而已。
有多特殊?
限于篇幅,此處不表,下篇剖析其深度原理??梢韵人伎紟讉€(gè)小問題:
- panic 究竟是啥?是一個(gè)結(jié)構(gòu)體?還是一個(gè)函數(shù)?
- 為什么 panic 會(huì)讓 Go 進(jìn)程退出的 ?
- 為什么 recover 一定要放在 defer 里面才生效?
- 為什么 recover 已經(jīng)放在 defer 里面,但是進(jìn)程還是沒有恢復(fù)?
- 為什么 panic 之后,還能再 panic ?有啥影響?
總結(jié)
- panic 產(chǎn)生的三大姿勢(shì):程序猿主動(dòng),編譯器輔助邏輯,軟中斷信號(hào)觸發(fā);
- 無(wú)論哪一種姿勢(shì),最終都是歸一到
panic( )
函數(shù)的處理,panic 只是語(yǔ)言層面的處理邏輯; - panic 發(fā)生之后,如果不做處理,默認(rèn)行為是打印 panic 原因,打印堆棧,進(jìn)程退出;
以上就是Go panic的三種產(chǎn)生方式細(xì)節(jié)探究的詳細(xì)內(nèi)容,更多關(guān)于Go panic產(chǎn)生方式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一文帶你了解Golang中強(qiáng)大的重試機(jī)制
在 Go 語(yǔ)言中,處理瞬態(tài)錯(cuò)誤是常見的挑戰(zhàn),這些錯(cuò)誤可能會(huì)在一段時(shí)間后自動(dòng)恢復(fù),因此,重試機(jī)制在這些情況下非常重要,所以本文就來(lái)和大家聊聊Golang中強(qiáng)大的重試機(jī)制吧2025-01-01自動(dòng)生成代碼controller?tool的簡(jiǎn)單使用
這篇文章主要為大家介紹了自動(dòng)生成代碼controller?tool的簡(jiǎn)單使用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05利用GO語(yǔ)言實(shí)現(xiàn)多人聊天室實(shí)例教程
聊天室的實(shí)現(xiàn)大家應(yīng)該都遇到過,這篇文章主要給大家介紹了關(guān)于利用GO語(yǔ)言實(shí)現(xiàn)多人聊天室的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧。2018-03-03使用Go語(yǔ)言編寫一個(gè)NTP服務(wù)器的流程步驟
NTP服務(wù)器【Network?Time?Protocol(NTP)】是用來(lái)使計(jì)算機(jī)時(shí)間同步化的一種協(xié)議,為了確保封閉局域網(wǎng)內(nèi)多個(gè)服務(wù)器的時(shí)間同步,我們計(jì)劃部署一個(gè)網(wǎng)絡(luò)時(shí)間同步服務(wù)器(NTP服務(wù)器),本文給大家介紹了使用Go語(yǔ)言編寫一個(gè)NTP服務(wù)器的流程步驟,需要的朋友可以參考下2024-11-11詳解Go語(yǔ)言如何使用xorm實(shí)現(xiàn)讀取mysql
xorm是go語(yǔ)言的常用orm之一,可以用來(lái)操作數(shù)據(jù)庫(kù)。本文就來(lái)和大家聊聊Go語(yǔ)言如何使用xorm實(shí)現(xiàn)讀取mysql功能,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-11-11利用go-zero在Go中快速實(shí)現(xiàn)JWT認(rèn)證的步驟詳解
這篇文章主要介紹了如何利用go-zero在Go中快速實(shí)現(xiàn)JWT認(rèn)證,本文分步驟通過實(shí)例圖文相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2020-10-10