網上有一道關于panic和defer的判斷題:“【中級】當程式運作時,如果遇到引用空指針、下标越界或顯式調用panic函數等情況,則先觸發panic函數的執行,然後調用延遲函數。調用者繼續傳遞panic,是以該過程一直在調用棧中重複發生:函數停止執行,調用延遲執行函數。如果一路在延遲函數中沒有recover函數的調用,則會到達該攜程的起點,該攜程結束,然後終止其他所有攜程,其他協程的終止過程也是重複發生:函數停止執行,調用延遲執行函數()”
這道題的考察點很多,要了解整個panic和defer的實作機制才能把這道題完全答對。首先把這段話切分成幾個點:
- 顯式調用panic函數等情況,則先觸發panic函數的執行,然後調用延遲函數(引用空指針、下标越界的情況這裡暫時不寫)
- 調用者繼續傳遞panic,是以該過程一直在調用棧中重複發生:函數停止執行,調用延遲執行函數。如果一路在延遲函數中沒有recover函數的調用,則會到達該攜程的起點
- 該協程結束,然後終止其他所有協程,其他協程的終止過程也是重複發生:函數停止執行,調用延遲執行函數
1)先觸發panic函數還是先調用延遲函數
如下例所示,example.go
package main
import (
"fmt"
)
func defunc1() {
fmt.Printf("defunc1 is called\n")
}
func test() {
defer defunc1()
panic("Not working!!")
}
func main() {
test()
}
輸出:
[[email protected] example]# go run example.go
defunc1 is called
panic: Not working!!
goroutine 1 [running]:
main.test()
/home/go-test/src/example/example.go:13 +0x5f
main.main()
/home/go-test/src/example/example.go:17 +0x20
exit status 2
從輸出來看defunc1先列印輸出,而panic後列印輸出,貌似defunc延遲函數先調用。可是,如果不先調用panic,那正常的執行流程又是怎麼被中斷的呢,這是一個沖突。
下面我們來看一下這段代碼的彙編代碼,為了簡化彙編代碼,稍作改動把對fmt的引用及調用換成對全局變量的指派:
package main
var g int
func defunc1() {
g = 13
}
func test() {
defer defunc1()
panic("Not working!!")
}
func main() {
g = 17
test()
}
通過指令“GOOS=linux GOARCH=386 go tool compile -S example.go > example.S”得到彙編代碼,其中test函數的代碼片段如下:
"".test STEXT size=96 args=0x0 locals=0x28
26 0x0000 00000 (example.go:9) TEXT "".test(SB), ABIInternal, $40-0
27 0x0000 00000 (example.go:9) MOVL TLS, CX
28 0x0007 00007 (example.go:9) MOVL (CX)(TLS*2), CX
29 0x000d 00013 (example.go:9) CMPL SP, 8(CX)
30 0x0010 00016 (example.go:9) JLS 89
31 0x0012 00018 (example.go:9) SUBL $40, SP
32 0x0015 00021 (example.go:9) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
33 0x0015 00021 (example.go:9) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
34 0x0015 00021 (example.go:9) FUNCDATA $2, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
35 0x0015 00021 (example.go:10) PCDATA $0, $0
36 0x0015 00021 (example.go:10) PCDATA $1, $0
37 0x0015 00021 (example.go:10) MOVL $0, ""..autotmp_0+8(SP)
38 0x001d 00029 (example.go:10) PCDATA $0, $1
39 0x001d 00029 (example.go:10) LEAL "".defunc1·f(SB), AX
40 0x0023 00035 (example.go:10) PCDATA $0, $0
41 0x0023 00035 (example.go:10) MOVL AX, ""..autotmp_0+24(SP)
42 0x0027 00039 (example.go:10) PCDATA $0, $1
43 0x0027 00039 (example.go:10) LEAL ""..autotmp_0+8(SP), AX
44 0x002b 00043 (example.go:10) PCDATA $0, $0
45 0x002b 00043 (example.go:10) MOVL AX, (SP)
46 0x002e 00046 (example.go:10) CALL runtime.deferprocStack(SB) //延遲函數入先進後出隊列
47 0x0033 00051 (example.go:10) TESTL AX, AX
48 0x0035 00053 (example.go:10) JNE 79
49 0x0037 00055 (example.go:11) PCDATA $0, $1
50 0x0037 00055 (example.go:11) LEAL type.string(SB), AX
51 0x003d 00061 (example.go:11) PCDATA $0, $0
52 0x003d 00061 (example.go:11) MOVL AX, (SP)
53 0x0040 00064 (example.go:11) PCDATA $0, $1
54 0x0040 00064 (example.go:11) LEAL ""..stmp_0(SB), AX
55 0x0046 00070 (example.go:11) PCDATA $0, $0
56 0x0046 00070 (example.go:11) MOVL AX, 4(SP)
57 0x004a 00074 (example.go:11) CALL runtime.gopanic(SB) // panic編譯成對gopanic的調用
58 0x004f 00079 (example.go:10) XCHGL AX, AX
59 0x0050 00080 (example.go:10) CALL runtime.deferreturn(SB) //隻要有defer函數,編譯器就會在函數傳回前插入對延遲函數隊列的執行,直到隊列為空
60 0x0055 00085 (example.go:10) ADDL $40, SP
61 0x0058 00088 (example.go:10) RET //函數傳回
62 0x0059 00089 (example.go:10) NOP
63 0x0059 00089 (example.go:9) PCDATA $1, $-1
64 0x0059 00089 (example.go:9) PCDATA $0, $-1
65 0x0059 00089 (example.go:9) CALL runtime.morestack_noctxt(SB)
66 0x005e 00094 (example.go:9) JMP 0
這段彙編裡有3個重要函數,都在runtime.go源檔案裡。
- runtime.deferprocStack: 對應的是(example.go:10)defer defunc1這一句,把延遲函數加入到該協程對應的一個延遲函數連結清單,每個協程各自維護一個延遲函數連結清單。
func deferprocStack(d *_defer) { gp := getg() //擷取目前協程,不同的協程維護不同的延遲函數連結清單 ... //把d置成新的連結清單表頭 *(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer)) *(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d)) return0() }
- runtime.gopanic,對應的是(example.go:11)panic("Not working!!")這一句,如果是系統級别異常(如panic during malloc)則直接退出程序不會執行任何延遲函數。如果是非系統級别異常,則按先進後出順序調用runtime.deferreturn來逐個執行本協程的延遲函數連結清單,如果某個被執行的延遲函數包含有對recover的調用,則恢複正常。如果都沒有對recover的調用,則列印輸出panic的消息如這裡的"Not working!!",然後列印stack trace,最後調用exit(2)結束程序。
func gopanic(e interface{}) { gp := getg() //擷取目前協程 ... for { d := gp._defer //擷取目前協程的延遲函數連結清單頭 ... //調用延遲函數 reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) ... gp._defer = d.link //移動到延遲函數連結清單的下一個節點 freedefer(d) //釋放連結清單節點d if p.recovered { ... mcall(recovery) //恢複協程 ... } } preprintpanics(gp._panic) //列印panic消息 fatalpanic(gp._panic) // 列印stack trace,退出程序 *(*int)(nil) = 0 // not reached }
func fatalpanic(msgs *_panic) { ... systemstack(func() { exit(2) //退出程序 }) *(*int)(nil) = 0 // not reached }
- runtime.deferreturn,這個是編譯器追加的,如果已經有panic發生,則在第2步就已經調用過deferreturn了,延遲函數連結清單已經為空。如果沒有panic發生,則執行延遲函數連結清單裡的延遲函數。
由此可見,在調用順序上是先調用panic對應的gopanic函數, 由gopanic函數去執行延遲函數,是以“先觸發panic函數的執行,然後調用延遲函數”是正确的。
2)子函數是否會把panic傳遞給父函數
同一個協程的所有延遲函數會維護在同一個連結清單當中,當子函數觸發panic即調用runtime.gopanic函數時,它會執行完連結清單裡的所有延遲函數,直到遇到某個延遲函數包含有對recover的調用,否則直接退出程序,注意是退出程序而不是協程。是以,panic是不會在父子函數之間傳遞的。
是以“調用者繼續傳遞panic,是以該過程一直在調用棧中重複發生:函數停止執行,調用延遲執行函數。如果一路在延遲函數中沒有recover函數的調用,則會到達該攜程的起點”這一句是錯誤的。
3)觸發panic的協程是如何停止所有協程的
gopanic函數調用fatalpanic,再由fatalpanic調用systemstack,systemstack最終調用exit(2),退出“程序”,而不是僅僅退出“協程”,其它協程的延遲函數不會被執行。這裡的exit在不同的作業系統上對應的函數不同,例如在windows上它對應的是
src/runtime/os_windows.go +556調用windows作業系統函數_ExitProcess(Ends the calling process and all its threads.)退出程序。
func exit(code int32) {
atomic.Store(&exiting, 1)
stdcall1(_ExitProcess, uintptr(code))
}
下面的代碼可以驗證其它協程的延遲函數是否會執行:
package main
import (
"fmt"
"time"
)
func defunc1() {
fmt.Printf("defunc1 is called\n")
}
func defunc2() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic that is defunc2: ", r)
}
fmt.Printf("defunc2 is called\n")
}
func test() {
defer defunc1()
panic("Not working!!")
}
func main() {
defer defunc2()
go func() {
test()
}()
time.Sleep(3 * time.Second)
fmt.Printf("main exit -----------")
}
輸出:
defunc1 is called
panic: Not working!!
goroutine 6 [running]:
main.test()
/home/zhoupeng/go-test/src/example/m.go:22 +0x5f
main.main.func1()
/home/zhoupeng/go-test/src/example/m.go:29 +0x20
created by main.main
/home/zhoupeng/go-test/src/example/m.go:28 +0x6d
exit status 2
可見,如果另起一個協程來調用test(),則當test()觸發panic時,main函數裡的延遲函數不會被執行。如果上面的代碼對test()的調用改成放在main函數所在的協程:
func main() {
defer defunc2()
//go func() {
test()
//}()
time.Sleep(3 * time.Second)
fmt.Printf("main exit -----------")
}
則輸出:
defunc1 is called
Recovered from panic that is defunc2: Not working!!
defunc2 is called
是以“該協程結束,然後終止其他所有協程,其他協程的終止過程也是重複發生:函數停止執行,調用延遲執行函數”這句是錯誤的。
4)panic之後的defer函數是否會被執行
題目裡并沒有問這一點,但這也是panic/defer機制的一部分。例如在panic之後再增加兩個defer:defer defunc3()和defer defunc4()
func test() {
defer defunc1()
panic("Not working!!")
defer defunc3()
}
func main() {
defer defunc2()
test()
defer defunc4()
}
從彙編代碼,Go的源碼和輸出結果來看,defer defunc3()由于和panic處在同一個test()函數并且在panic之後,是以會被編譯器在編譯過程中直接忽略掉;而defer defunc4()會被編譯成runtime.deferprocStack,但是在panic觸發後程序直接退出,defunc4的runtime.deferprocStack還沒有被調用,還沒有進入延遲隊列,是以不會被執行。
5) panic觸發之後defer函數裡再次觸發新的panic,之前的panic怎麼處理
例如在下面的代碼中,panic1觸發之後執行最後一個defer再觸發panic2, panic2觸發後再執行倒數第二個defer進而觸發panic3,而Go的機制是如果目前defer函數是由之前的panic觸發的并且目前defer函數會觸發新的panic,則前一個panic停止執行。也就是說下面代碼中當panic2觸發時,panic1的執行過程将停止,當panic3觸發時panic2的執行過程将停止,是以最終能被recover捕獲到的隻有panic3。
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
defer func() {
panic("panic3")
}()
defer func() {
panic("panic2")
}()
panic("panic1")
}
輸出:
panic3
這部分源碼在“src/runtime/panic.go”的gopanic函數裡。它的注釋寫的比較清楚,當我們因為觸發了一個新的panic而再次運作到這裡時,如果這個defer是由之前的panic調用起來的,則前一個panic停止執行。
// If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),
// take defer off list. The earlier panic or Goexit will not continue running.
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
注:以上測試的Go版本是1.13.8