天天看點

Go面試:從一道判斷題來談panic和defer的調用機制和執行順序

網上有一道關于panic和defer的判斷題:“【中級】當程式運作時,如果遇到引用空指針、下标越界或顯式調用panic函數等情況,則先觸發panic函數的執行,然後調用延遲函數。調用者繼續傳遞panic,是以該過程一直在調用棧中重複發生:函數停止執行,調用延遲執行函數。如果一路在延遲函數中沒有recover函數的調用,則會到達該攜程的起點,該攜程結束,然後終止其他所有攜程,其他協程的終止過程也是重複發生:函數停止執行,調用延遲執行函數()”

這道題的考察點很多,要了解整個panic和defer的實作機制才能把這道題完全答對。首先把這段話切分成幾個點:

  1. 顯式調用panic函數等情況,則先觸發panic函數的執行,然後調用延遲函數(引用空指針、下标越界的情況這裡暫時不寫)
  2. 調用者繼續傳遞panic,是以該過程一直在調用棧中重複發生:函數停止執行,調用延遲執行函數。如果一路在延遲函數中沒有recover函數的調用,則會到達該攜程的起點
  3. 該協程結束,然後終止其他所有協程,其他協程的終止過程也是重複發生:函數停止執行,調用延遲執行函數

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源檔案裡。

  1. 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()
     }
               
  2. 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
    }
               
  3. 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

繼續閱讀