Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

恐慌與恢復 - panic/recover

我們知道Go語言中許多錯誤會在編譯時暴露出來,直接編譯不通過,但對於空指針訪問元素,切片/數組越界訪問之類的運行時錯誤,只會在運行時引發 panic 異常暴露出來。這種由Go語言自動的觸發的 panic 異常屬於運行時panic(Run-time panics)1。當發生 panic 時候,Go會運行所有已經註冊的延遲函數,若延遲函數中未進行panic異常捕獲處理,那麼最終Go進程會終止,並打印堆棧信息。此外Go中還內置了 panic 函數,可以用於用戶手動觸發panic

Go語言中內置的 recover 函數可以用來捕獲 panic異常,但 recover 函數只能放在延遲函數調用中,才能起作用。我們從之前的章節《基礎篇-語言特性-defer函數 》瞭解到,多個延遲函數,會組成一個鏈表。Go在發生panic過程中,會依次遍歷該鏈表,並檢查鏈表中的延遲函數是否調用了 recover 函數調用,若調用了則 panic 異常會被捕獲而不會繼續向上拋出,否則會繼續向上拋出異常和執行延遲函數,直到該 panic 沒有被捕獲,進程異常終止,這個過程叫做panicking。我們需要知道的是即使panic被延遲函數鏈表中某個延遲函數捕獲處理了,但其他的延遲函數還是會繼續執行的,只是panic異常不在繼續拋出

接下來我們來將深入瞭解下panic和recover底層的實現機制。在開始之前,我們來看下下面的測試題。

測試題:下面哪些panic異常將會捕獲?

case 1:

func main() {
    recover()
    panic("it is panic") // not recover
}

case 2:

func main() {
    defer func() {
        recover()
    }()

    panic("it is panic") // recover
}

case 3:

func main() {
	defer recover()
	panic("it is panic") // not recover
}

case 4:

func main() {
    defer func() {
        defer recover()
    }()

    panic("it is panic") // recover
}

case 5:

func main() {
	defer func() {
		defer func() {
			recover()
		}()
	}()

	panic("it is panic") // not recover
}

case 6:

func main() {
	defer doRecover()
	panic("it is panic") // recover
}

func doRecover() {
	recover()
	fmt.Println("hello")
}

case 7:

func main() {
	defer doRecover()
	panic("it is panic") // recover
}

func doRecover() {
	defer recover()
}

簡單說明下上面幾個案例運行結果:

  • case 1中recover函數調用不是在defer延遲函數裏面,肯定不會捕獲panic異常。
  • case 2中是panic異常捕獲的標準操作,是可以捕獲panic異常的,case 6case 2是一樣的,只不過一個是匿名延遲函數,一個是具名延遲函數,同樣可以捕獲panic異常。
  • case 3中recover函數作爲延遲函數,沒有在其他延遲函數中調用,它也是不起作用的。
  • case 4中recover函數被一個延遲函數調用,且recover函數本身作爲一個延遲函數,這個情況下也是可以正常捕獲panic異常的,case 7case 4是一樣的,只不過一個是匿名延遲函數,一個是具名延遲函數,同樣可以捕獲panic異常。
  • case 5中儘管recover函數被延遲函數調用,但它卻無法捕獲panic異常。

從上面案例中可以看出來,使用recover函數進行panic異常捕獲,也要使用正確才能起作用。下面會分析源碼,探討panic-recover實現機制,也能更好幫助你理解爲什麼case 2,case 4可以起作用,而case 3case 5爲啥沒有起作用。

源碼分析

我們先分析case 2案例,我們可以通過go tool compile -N -l -S case2.go獲取彙編代碼,來查看panic和recover在底層真正的實現:

main_pc0:
	TEXT    "".main(SB), ABIInternal, $104-0
	MOVQ    (TLS), CX
	CMPQ    SP, 16(CX)
	JLS     main_pc113
	SUBQ    $104, SP
	MOVQ    BP, 96(SP)
	LEAQ    96(SP), BP
	MOVL    $0, ""..autotmp_1+16(SP)
	LEAQ    "".main.func1·f(SB), AX
	MOVQ    AX, ""..autotmp_1+40(SP)
	LEAQ    ""..autotmp_1+16(SP), AX
	MOVQ    AX, (SP)
	CALL    runtime.deferprocStack(SB)
	TESTL   AX, AX
	JNE     main_pc97
	JMP     main_pc69
main_pc69:
	LEAQ    type.string(SB), AX
	MOVQ    AX, (SP)
	LEAQ    ""..stmp_0(SB), AX
	MOVQ    AX, 8(SP)
	CALL    runtime.gopanic(SB)
main_pc97:
	XCHGL   AX, AX
	CALL    runtime.deferreturn(SB)
	MOVQ    96(SP), BP
	ADDQ    $104, SP
	RET
main_pc113:
	NOP
	CALL    runtime.morestack_noctxt(SB)
	JMP     main_pc0
main_func1_pc0:
	TEXT    "".main.func1(SB), ABIInternal, $32-0
	MOVQ    (TLS), CX
	CMPQ    SP, 16(CX)
	JLS     main_func1_pc53
	SUBQ    $32, SP
	MOVQ    BP, 24(SP)
	LEAQ    24(SP), BP
	LEAQ    ""..fp+40(SP), AX
	MOVQ    AX, (SP)
	CALL    runtime.gorecover(SB)
	MOVQ    24(SP), BP
	ADDQ    $32, SP
	RET
main_func1_pc53:
	NOP
	CALL    runtime.morestack_noctxt(SB)
	JMP     main_func1_pc0

從上面彙編代碼中,可以看出 panic 函數底層實現 runtime.gopanicrecover 函數底層實現是 runtime.gorecover

panic函數底層實現的 runtime.gopanic 源碼如下:

func gopanic(e interface{}) {
	gp := getg()
	
	... // 一些判斷當前g是否允許在用戶棧,是否正在內存分配的代碼,略
	
	var p _panic // panic底層數據結構是_panic
	p.arg = e // e是panic函數的參數,對應case2中的: it is panic
	p.link = gp._panic
	gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) // 將當前panic掛到g上面

	atomic.Xadd(&runningPanicDefers, 1) // 記錄正在執行panic的goroutine數量,防止main groutine返回時候,
	// 其他goroutine的panic棧信息未打印出來。@see https://github.com/golang/go/blob/go1.14.13/src/runtime/proc.go#L208-L220

	
	// 對於open-coded defer實現的延遲函數,需要掃描FUNCDATA_OpenCodedDeferInfo信息,
	// 獲取延遲函數的sp/pc信息,並創建_defer結構,將其插入gp._defer鏈表中
	// 這是也是在defer函數章節中,提到的爲啥open-coded defer提升了延遲函數的性能,而panic性能卻降低的原因
	addOneOpenDeferFrame(gp, getcallerpc(), unsafe.Pointer(getcallersp()))

	for { // 開始遍歷defer鏈表
		d := gp._defer
		if d == nil {
			break
		}

	
		// 當延遲函數裏面再次拋出panic或者調用runtime.Goexit時候,
		// 會再次進入同一個延遲函數,此時d.started已經設置爲true狀態
		if d.started {
			if d._panic != nil { // 標記上一個_panic狀態爲aborted
				d._panic.aborted = true
			}
			d._panic = nil
			if !d.openDefer {
				// 對於非open-coded defer函數,我們需要將_defer從gp._defer鏈表中溢出去,防止繼續重複執行
				d.fn = nil
				gp._defer = d.link
				freedefer(d)
				continue
			}
		}

	
		// 標記當前defer開始執行,這樣當g棧增長時候或者垃圾回收時候,可以更新defer的參數棧幀
		d.started = true

		// 記錄當前的_panic信息到_defer結構中,這樣當該defer函數再次發生panic時候,可以標記d._panic爲aborted狀態
		d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

		done := true
		if d.openDefer { // 如果該延遲函數是open-coded defer函數
			done = runOpenDeferFrame(gp, d) // 運行open-coded defer函數,如果當前棧下面沒有其他延遲函數,則返回true
			if done && !d._panic.recovered { // 如果當前棧下面沒有其他open-coded defer函數了,且panic也未recover,
			// 那麼繼續當前的open-coded defer函數的sp作爲基址,繼續掃描funcdata,獲取open-coded defer函數。
			// 之所以這麼做是因爲open-coded defer裏面也存在defer函數的情況,例如case4
				addOneOpenDeferFrame(gp, 0, nil)
			}
		} else {// 非open-coded defer實現的defer函數

			// getargp返回其caller的保存callee參數的地址。
			// 之前介紹過了Go語言中函數調用約定,callee的參數存儲,是由caller的棧空間提供。
			p.argp = unsafe.Pointer(getargp(0)) // 這裏面p.argp保存的gopanic函數作爲caller時候,保存callee參數的地址。
			// 之所以要_panic.argp保存gopanic的callee參數地址,
			// 這是因爲調用gorecover會通過此檢查其caller的caller是不是gopanic。
			// 這也是case5等不能捕獲panic異常的原因。

			// 調用defer函數
			reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
		}
		p.argp = nil

		// reflectcall did not panic. Remove d.
		if gp._defer != d {
			throw("bad defer entry in panic")
		}
		d._panic = nil

		pc := d.pc
		sp := unsafe.Pointer(d.sp)
		if done { // 從gp._defer鏈表清除掉當前defer函數
			d.fn = nil
			gp._defer = d.link
			freedefer(d)
		}
		if p.recovered {
			gp._panic = p.link
			if gp._panic != nil && gp._panic.goexit && gp._panic.aborted {
				// A normal recover would bypass/abort the Goexit.  Instead,
				// we return to the processing loop of the Goexit.
				gp.sigcode0 = uintptr(gp._panic.sp)
				gp.sigcode1 = uintptr(gp._panic.pc)
				mcall(recovery)
				throw("bypassed recovery failed") // mcall should not return
			}
			atomic.Xadd(&runningPanicDefers, -1)

			if done { // panic已經被recover處理掉了,那麼移除掉上面通過addOneOpenDeferFrame添加到gp._defer中的open-coded defer函數。
			// 因爲這些open-coded defer是通過inline方式執行的,從gp._defer鏈表中移除掉,不影響它們繼續的執行
				d := gp._defer
				var prev *_defer
				for d != nil {
					if d.openDefer {
						if d.started {
							break
						}
						if prev == nil {
							gp._defer = d.link
						} else {
							prev.link = d.link
						}
						newd := d.link
						freedefer(d)
						d = newd
					} else {
						prev = d
						d = d.link
					}
				}
			}

			gp._panic = p.link // 無用代碼,上面已經操作過了
			// Aborted panics are marked but remain on the g.panic list.
			// Remove them from the list.
			for gp._panic != nil && gp._panic.aborted {
				gp._panic = gp._panic.link
			}
			if gp._panic == nil { // must be done with signal
				gp.sig = 0
			}
			// Pass information about recovering frame to recovery.
			gp.sigcode0 = uintptr(sp)
			gp.sigcode1 = pc
			mcall(recovery)
			throw("recovery failed") // mcall should not return
		}
	}

	preprintpanics(gp._panic)

	fatalpanic(gp._panic) // should not return
	*(*int)(nil) = 0      // not reached
}

對於基於open-coded defer方式實現的延遲函數中處理panic recover邏輯,比如addOneOpenDeferFrame,runOpenDeferFrame等函數,這裏不再深究。這裏主要分析通過鏈表實現的延遲函數中處理panic recover邏輯。

接下來我們看下recover函數底層實現runtime.gorecover源碼

func gorecover(argp uintptr) interface{} {
	gp := getg()
	p := gp._panic
	if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
		p.recovered = true
		return p.arg
	}
	return nil
}

  1. Go官方語法指南:運行時恐慌