主頁 > 知識庫 > 詳解go中panic源碼解讀

詳解go中panic源碼解讀

熱門標(biāo)簽:壽光微信地圖標(biāo)注 excel地圖標(biāo)注分布數(shù)據(jù) 外呼系統(tǒng)顯本地手機(jī)號 涿州代理外呼系統(tǒng) 外呼系統(tǒng)用什么卡 評價高的400電話辦理 電話機(jī)器人軟件免費 百度地圖標(biāo)注后傳給手機(jī) 阿克蘇地圖標(biāo)注

panic源碼解讀

前言

本文是在go version go1.13.15 darwin/amd64上進(jìn)行的

panic的作用

  • panic能夠改變程序的控制流,調(diào)用panic后會立刻停止執(zhí)行當(dāng)前函數(shù)的剩余代碼,并在當(dāng)前Goroutine中遞歸執(zhí)行調(diào)用方的defer;
  • recover可以中止panic造成的程序崩潰。它是一個只能在defer中發(fā)揮作用的函數(shù),在其他作用域中調(diào)用不會發(fā)揮作用;

舉個栗子

package main

import "fmt"

func main() {
	fmt.Println(1)
	func() {
		fmt.Println(2)
		panic("3")
	}()
	fmt.Println(4)
}

輸出

1
2
panic: 3

goroutine 1 [running]:
main.main.func1(...)
        /Users/yj/Go/src/Go-POINT/panic/main.go:9
main.main()
        /Users/yj/Go/src/Go-POINT/panic/main.go:10 +0xee

panic后會立刻停止執(zhí)行當(dāng)前函數(shù)的剩余代碼,所以4沒有打印出來

對于recover

  • panic只會觸發(fā)當(dāng)前Goroutine的defer;
  • recover只有在defer中調(diào)用才會生效;
  • panic允許在defer中嵌套多次調(diào)用;
package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println(1)

	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()

	go func() {
		fmt.Println(2)
		panic("3")
	}()
	time.Sleep(time.Second)
	fmt.Println(4)
}

上面的栗子,因為recoverpanic不在同一個goroutine中,所以不會捕獲到

嵌套的demo

func main() {
	defer fmt.Println("in main")
	defer func() {
		defer func() {
			panic("3 panic again and again")
		}()
		panic("2 panic again")
	}()

	panic("1 panic once")
}

輸出

in main
panic: 1 panic once
        panic: 2 panic again
        panic: 3 panic again and again

goroutine 1 [running]:
...

多次調(diào)用panic也不會影響defer函數(shù)的正常執(zhí)行,所以使用defer進(jìn)行收尾工作一般來說都是安全的。

panic使用場景

  • error:可預(yù)見的錯誤
  • panic:不可預(yù)見的異常

需要注意的是,你應(yīng)該盡可能地使用error,而不是使用panicrecover。只有當(dāng)程序不能繼續(xù)運(yùn)行的時候,才應(yīng)該使用panicrecover機(jī)制。

panic有兩個合理的用例。

1、發(fā)生了一個不能恢復(fù)的錯誤,此時程序不能繼續(xù)運(yùn)行。 一個例子就是 web 服務(wù)器無法綁定所要求的端口。在這種情況下,就應(yīng)該使用 panic,因為如果不能綁定端口,啥也做不了。

2、發(fā)生了一個編程上的錯誤。 假如我們有一個接收指針參數(shù)的方法,而其他人使用 nil 作為參數(shù)調(diào)用了它。在這種情況下,我們可以使用panic,因為這是一個編程錯誤:用 nil 參數(shù)調(diào)用了一個只能接收合法指針的方法。

在一般情況下,我們不應(yīng)通過調(diào)用panic函數(shù)來報告普通的錯誤,而應(yīng)該只把它作為報告致命錯誤的一種方式。當(dāng)某些不應(yīng)該發(fā)生的場景發(fā)生時,我們就應(yīng)該調(diào)用panic。

總結(jié)下panic的使用場景:

1、空指針引用

2、下標(biāo)越界

3、除數(shù)為0

4、不應(yīng)該出現(xiàn)的分支,比如default

5、輸入不應(yīng)該引起函數(shù)錯誤

看下實現(xiàn)

先來看下_panic的結(jié)構(gòu)

// _panic 保存了一個活躍的 panic
//
// 這個標(biāo)記了 go:notinheap 因為 _panic 的值必須位于棧上
//
// argp 和 link 字段為棧指針,但在棧增長時不需要特殊處理:因為他們是指針類型且
// _panic 值只位于棧上,正常的棧指針調(diào)整會處理他們。
//
//go:notinheap
type _panic struct {
	argp      unsafe.Pointer // panic 期間 defer 調(diào)用參數(shù)的指針; 無法移動 - liblink 已知
	arg       interface{}    // panic的參數(shù)
	link      *_panic        // link 鏈接到更早的 panic
	recovered bool           // panic是否結(jié)束
	aborted   bool           // panic是否被忽略
}

link指向了保存在goroutine鏈表中先前的panic鏈表

gopanic

編譯器會將panic裝換成gopanic,來看下執(zhí)行的流程:

1、創(chuàng)建新的runtime._panic并添加到所在Goroutine的_panic鏈表的最前面;

2、在循環(huán)中不斷從當(dāng)前Goroutine 的_defer中鏈表獲取runtime._defer并調(diào)用runtime.reflectcall運(yùn)行延遲調(diào)用函數(shù);

3、調(diào)用runtime.fatalpanic中止整個程序;

// 預(yù)先聲明的函數(shù) panic 的實現(xiàn)
func gopanic(e interface{}) {
	gp := getg()
	// 判斷在系統(tǒng)棧上還是在用戶棧上
	// 如果執(zhí)行在系統(tǒng)或信號棧時,getg() 會返回當(dāng)前 m 的 g0 或 gsignal
	// 因此可以通過 gp.m.curg == gp 來判斷所在棧
	// 系統(tǒng)棧上的 panic 無法恢復(fù)
	if gp.m.curg != gp {
		print("panic: ")
		printany(e)
		print("\n")
		throw("panic on system stack")
	}
	// 如果正在進(jìn)行 malloc 時發(fā)生 panic 也無法恢復(fù)
	if gp.m.mallocing != 0 {
		print("panic: ")
		printany(e)
		print("\n")
		throw("panic during malloc")
	}
	// 在禁止搶占時發(fā)生 panic 也無法恢復(fù)
	if gp.m.preemptoff != "" {
		print("panic: ")
		printany(e)
		print("\n")
		print("preempt off reason: ")
		print(gp.m.preemptoff)
		print("\n")
		throw("panic during preemptoff")
	}
	// 在 g 鎖在 m 上時發(fā)生 panic 也無法恢復(fù)
	if gp.m.locks != 0 {
		print("panic: ")
		printany(e)
		print("\n")
		throw("panic holding locks")
	}

	// 下面是可以恢復(fù)的
	var p _panic
	p.arg = e
	// panic 保存了對應(yīng)的消息,并指向了保存在 goroutine 鏈表中先前的 panic 鏈表
	p.link = gp._panic
	gp._panic = (*_panic)(noescape(unsafe.Pointer(p)))

	atomic.Xadd(runningPanicDefers, 1)

	for {
		// 開始逐個取當(dāng)前 goroutine 的 defer 調(diào)用
		d := gp._defer
		// 沒有defer,退出循環(huán)
		if d == nil {
			break
		}

		// 如果 defer 是由早期的 panic 或 Goexit 開始的(并且,因為我們回到這里,這引發(fā)了新的 panic),
		// 則將 defer 帶離鏈表。更早的 panic 或 Goexit 將無法繼續(xù)運(yùn)行。
		if d.started {
			if d._panic != nil {
				d._panic.aborted = true
			}
			d._panic = nil
			d.fn = nil
			gp._defer = d.link
			freedefer(d)
			continue
		}

		// 將deferred標(biāo)記為started
		// 如果棧增長或者垃圾回收在 reflectcall 開始執(zhí)行 d.fn 前發(fā)生
		// 標(biāo)記 defer 已經(jīng)開始執(zhí)行,但仍將其保存在列表中,從而 traceback 可以找到并更新這個 defer 的參數(shù)幀

		// 標(biāo)記defer是否已經(jīng)執(zhí)行
		d.started = true

		// 記錄正在運(yùn)行的延遲的panic。
		// 如果在延遲調(diào)用期間有新的panic,那么這個panic
		// 將在列表中找到d,并將標(biāo)記d._panic(此panic)中止。
		d._panic = (*_panic)(noescape(unsafe.Pointer(p)))

		p.argp = unsafe.Pointer(getargp(0))

		reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
		p.argp = nil

		// reflectcall沒有panic。刪除d
		if gp._defer != d {
			throw("bad defer entry in panic")
		}
		d._panic = nil
		d.fn = nil
		gp._defer = d.link

		// trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
		//GC()

		pc := d.pc
		sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
		freedefer(d)
		if p.recovered {
			atomic.Xadd(runningPanicDefers, -1)

			gp._panic = p.link
			// 忽略的 panic 會被標(biāo)記,但仍然保留在 g.panic 列表中
			// 這里將它們移出列表
			for gp._panic != nil  gp._panic.aborted {
				gp._panic = gp._panic.link
			}
			if gp._panic == nil { // 必須由 signal 完成
				gp.sig = 0
			}
			// 傳遞關(guān)于恢復(fù)幀的信息
			gp.sigcode0 = uintptr(sp)
			gp.sigcode1 = pc
			// 調(diào)用 recover,并重新進(jìn)入調(diào)度循環(huán),不再返回
			mcall(recovery)
			// 如果無法重新進(jìn)入調(diào)度循環(huán),則無法恢復(fù)錯誤
			throw("recovery failed") // mcall should not return
		}
	}

	// 消耗完所有的 defer 調(diào)用,保守地進(jìn)行 panic
	// 因為在凍結(jié)之后調(diào)用任意用戶代碼是不安全的,所以我們調(diào)用 preprintpanics 來調(diào)用
	// 所有必要的 Error 和 String 方法來在 startpanic 之前準(zhǔn)備 panic 字符串。
	preprintpanics(gp._panic)

	fatalpanic(gp._panic) // 不應(yīng)該返回
	*(*int)(nil) = 0      // 無法觸及
}

// reflectcall 使用 arg 指向的 n 個參數(shù)字節(jié)的副本調(diào)用 fn。
// fn 返回后,reflectcall 在返回之前將 n-retoffset 結(jié)果字節(jié)復(fù)制回 arg+retoffset。
// 如果重新復(fù)制結(jié)果字節(jié),則調(diào)用者應(yīng)將參數(shù)幀類型作為 argtype 傳遞,以便該調(diào)用可以在復(fù)制期間執(zhí)行適當(dāng)?shù)膶懻系K。
// reflect 包傳遞幀類型。在 runtime 包中,只有一個調(diào)用將結(jié)果復(fù)制回來,即 cgocallbackg1,
// 并且它不傳遞幀類型,這意味著沒有調(diào)用寫障礙。參見該調(diào)用的頁面了解相關(guān)理由。
//
// 包 reflect 通過 linkname 訪問此符號
func reflectcall(argtype *_type, fn, arg unsafe.Pointer, argsize uint32, retoffset uint32)

梳理下流程

1、在處理panic期間,會先判斷當(dāng)前panic的類型,確定panic是否可恢復(fù);

  • 系統(tǒng)棧上的panic無法恢復(fù)
  • 如果正在進(jìn)行malloc時發(fā)生panic也無法恢復(fù)
  • 在禁止搶占時發(fā)生panic也無法恢復(fù)
  • 在g鎖在m上時發(fā)生panic也無法恢復(fù)

2、可恢復(fù)的panicpaniclink指向goroutine鏈表中先前的panic鏈表;

3、循環(huán)逐個獲取當(dāng)前goroutinedefer調(diào)用;

  • 如果defer是由早期panic或Goexit開始的,則將defer帶離鏈表,更早的panic或Goexit將無法繼續(xù)運(yùn)行,也就是將之前的panic終止掉,將aborted設(shè)置為true,在下面執(zhí)行recover時保證goexit不會被取消;
  • recovered會在gorecover中被標(biāo)記,見下文。當(dāng)recovered被標(biāo)記為true時,recovery函數(shù)觸發(fā)Goroutine的調(diào)度,調(diào)度之前會準(zhǔn)備好 sp、pc 以及函數(shù)的返回值;
  • 當(dāng)延遲函數(shù)中recover了一個panic時,就會返回1,當(dāng)runtime.deferproc函數(shù)的返回值是1時,編譯器生成的代碼會直接跳轉(zhuǎn)到調(diào)用方函數(shù)返回之前并執(zhí)行runtime.deferreturn,跳轉(zhuǎn)到runtime.deferturn函數(shù)之后,程序就已經(jīng)從panic恢復(fù)了正常的邏輯。而runtime.gorecover函數(shù)也能從runtime._panic結(jié)構(gòu)中取出了調(diào)用panic時傳入的arg參數(shù)并返回給調(diào)用方。
// 在發(fā)生 panic 后 defer 函數(shù)調(diào)用 recover 后展開棧。然后安排繼續(xù)運(yùn)行,
// 就像 defer 函數(shù)的調(diào)用方正常返回一樣。
func recovery(gp *g) {
	// Info about defer passed in G struct.
	sp := gp.sigcode0
	pc := gp.sigcode1

	// d's arguments need to be in the stack.
	if sp != 0  (sp  gp.stack.lo || gp.stack.hi  sp) {
		print("recover: ", hex(sp), " not in [", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n")
		throw("bad recovery")
	}

	// 使 deferproc 為此 d 返回
	// 這時候返回 1。調(diào)用函數(shù)將跳轉(zhuǎn)到標(biāo)準(zhǔn)的返回尾聲
	gp.sched.sp = sp
	gp.sched.pc = pc
	gp.sched.lr = 0
	gp.sched.ret = 1
	gogo(gp.sched)
}

recovery函數(shù)中,利用g中的兩個狀態(tài)碼回溯棧指針sp并恢復(fù)程序計數(shù)器pc到調(diào)度器中,并調(diào)用gogo重新調(diào)度g,將g恢復(fù)到調(diào)用recover函數(shù)的位置,goroutine繼續(xù)執(zhí)行,recovery在調(diào)度過程中會將函數(shù)的返回值設(shè)置為1。調(diào)用函數(shù)將跳轉(zhuǎn)到標(biāo)準(zhǔn)的返回尾聲。

func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
	...

	// deferproc returns 0 normally.
	// a deferred func that stops a panic
	// makes the deferproc return 1.
	// the code the compiler generates always
	// checks the return value and jumps to the
	// end of the function if deferproc returns != 0.
	return0()
	// No code can go here - the C return register has
	// been set and must not be clobbered.
}

當(dāng)延遲函數(shù)中recover了一個panic時,就會返回1,當(dāng)runtime.deferproc函數(shù)的返回值是1時,編譯器生成的代碼會直接跳轉(zhuǎn)到調(diào)用方函數(shù)返回之前并執(zhí)行runtime.deferreturn,跳轉(zhuǎn)到runtime.deferturn函數(shù)之后,程序就已經(jīng)從panic恢復(fù)了正常的邏輯。而runtime.gorecover函數(shù)也能從runtime._panic結(jié)構(gòu)中取出了調(diào)用panic時傳入的arg參數(shù)并返回給調(diào)用方。

gorecover

編譯器會將recover裝換成gorecover

如果recover被正確執(zhí)行了,也就是gorecover,那么recovered將被標(biāo)記成true

// go/src/runtime/panic.go
// 執(zhí)行預(yù)先聲明的函數(shù) recover。
// 不允許分段棧,因為它需要可靠地找到其調(diào)用者的棧段。
//
// TODO(rsc): Once we commit to CopyStackAlways,
// this doesn't need to be nosplit.
//go:nosplit
func gorecover(argp uintptr) interface{} {
	// 必須在 panic 期間作為 defer 調(diào)用的一部分在函數(shù)中運(yùn)行。
	// 必須從調(diào)用的最頂層函數(shù)( defer 語句中使用的函數(shù))調(diào)用。
	// p.argp 是最頂層 defer 函數(shù)調(diào)用的參數(shù)指針。
	// 比較調(diào)用方報告的 argp,如果匹配,則調(diào)用者可以恢復(fù)。
	gp := getg()
	p := gp._panic
	if p != nil  !p.recovered  argp == uintptr(p.argp) {
		// 標(biāo)記recovered
		p.recovered = true
		return p.arg
	}
	return nil
}

在正常情況下,它會修改runtime._panicrecovered字段,runtime.gorecover函數(shù)中并不包含恢復(fù)程序的邏輯,程序的恢復(fù)是由runtime.gopanic函數(shù)負(fù)責(zé)。

gorecoverrecovered標(biāo)記為true,然后gopanic就可以通過mcall調(diào)用recovery并重新進(jìn)入調(diào)度循環(huán)

fatalpanic

runtime.fatalpanic實現(xiàn)了無法被恢復(fù)的程序崩潰,它在中止程序之前會通過runtime.printpanics打印出全部的panic消息以及調(diào)用時傳入的參數(shù):

// go/src/runtime/panic.go
// fatalpanic 實現(xiàn)了不可恢復(fù)的 panic。類似于 fatalthrow,
// 如果 msgs != nil,則 fatalpanic 仍然能夠打印 panic 的消息
// 并在 main 在退出時候減少 runningPanicDeferss
//
//go:nosplit
func fatalpanic(msgs *_panic) {
	// 返回程序計數(shù)寄存器指針
	pc := getcallerpc()
	// 返回堆棧指針
	sp := getcallersp()
	// 返回當(dāng)前G
	gp := getg()
	var docrash bool
	// 切換到系統(tǒng)棧來避免棧增長,如果運(yùn)行時狀態(tài)較差則可能導(dǎo)致更糟糕的事情
	systemstack(func() {
		if startpanic_m()  msgs != nil {
			// 有 panic 消息和 startpanic_m 則可以嘗試打印它們

			// startpanic_m 設(shè)置 panic 會從阻止 main 的退出,
			// 因此現(xiàn)在可以開始減少 runningPanicDefers 了
			atomic.Xadd(runningPanicDefers, -1)

			printpanics(msgs)
		}

		docrash = dopanic_m(gp, pc, sp)
	})

	if docrash {
		// 通過在上述 systemstack 調(diào)用之外崩潰,調(diào)試器在生成回溯時不會混淆。
		// 函數(shù)崩潰標(biāo)記為 nosplit 以避免堆棧增長。
		crash()
	}
	// 從系統(tǒng)推出
	systemstack(func() {
		exit(2)
	})

	*(*int)(nil) = 0 // not reached
}

// 打印出當(dāng)前活動的panic
func printpanics(p *_panic) {
	if p.link != nil {
		printpanics(p.link)
		print("\t")
	}
	print("panic: ")
	printany(p.arg)
	if p.recovered {
		print(" [recovered]")
	}
	print("\n")
}

總結(jié)

引一段來自【panic 和recover】的總結(jié)

1、編譯器會負(fù)責(zé)做轉(zhuǎn)換關(guān)鍵字的工作;

1、將panicrecover分別轉(zhuǎn)換成runtime.gopanicruntime.gorecover

2、將defer轉(zhuǎn)換成runtime.deferproc函數(shù);

3、在調(diào)用defer的函數(shù)末尾調(diào)用runtime.deferreturn函數(shù);

2、在運(yùn)行過程中遇到runtime.gopanic方法時,會從Goroutine的鏈表依次取出runtime._defer結(jié)構(gòu)體并執(zhí)行;

3、如果調(diào)用延遲執(zhí)行函數(shù)時遇到了runtime.gorecover就會將_panic.recovered標(biāo)記成true并返回panic的參數(shù);

1、在這次調(diào)用結(jié)束之后,runtime.gopanic會從runtime._defer結(jié)構(gòu)體中取出程序計數(shù)器pc和棧指針sp并調(diào)用runtime.recovery函數(shù)進(jìn)行恢復(fù)程序;

2、runtime.recovery會根據(jù)傳入的pcsp跳轉(zhuǎn)回runtime.deferproc;

3、編譯器自動生成的代碼會發(fā)現(xiàn)runtime.deferproc的返回值不為0,這時會跳回runtime.deferreturn并恢復(fù)到正常的執(zhí)行流程;

4、如果沒有遇到runtime.gorecover就會依次遍歷所有的runtime._defer,并在最后調(diào)用runtime.fatalpanic中止程序、打印panic的參數(shù)并返回錯誤碼2

參考

【panic 和 recover】https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-panic-recover/
【恐慌與恢復(fù)內(nèi)建函數(shù)】https://golang.design/under-the-hood/zh-cn/part1basic/ch03lang/panic/
【Go語言panic/recover的實現(xiàn)】https://zhuanlan.zhihu.com/p/72779197
【panic and recover】https://eddycjy.gitbook.io/golang/di-6-ke-chang-yong-guan-jian-zi/panic-and-recover
【翻了源碼,我把 panic 與 recover 給徹底搞明白了】https://jishuin.proginn.com/p/763bfbd4ed8c

到此這篇關(guān)于詳解go中panic源碼解讀的文章就介紹到這了,更多相關(guān)go panic源碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

您可能感興趣的文章:
  • Go中recover與panic區(qū)別詳解
  • go panic時如何讓函數(shù)返回數(shù)據(jù)?
  • Golang捕獲panic堆棧信息的講解
  • go語言的panic和recover函數(shù)用法實例
  • go語言異常panic和恢復(fù)recover用法實例
  • GO語言異常處理機(jī)制panic和recover分析

標(biāo)簽:汕頭 重慶 吐魯番 梅河口 欽州 銅川 蘭州 雞西

巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《詳解go中panic源碼解讀》,本文關(guān)鍵詞  詳解,中,panic,源碼,解讀,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問題,煩請?zhí)峁┫嚓P(guān)信息告之我們,我們將及時溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無關(guān)。
  • 相關(guān)文章
  • 下面列出與本文章《詳解go中panic源碼解讀》相關(guān)的同類信息!
  • 本頁收集關(guān)于詳解go中panic源碼解讀的相關(guān)信息資訊供網(wǎng)民參考!
  • 推薦文章