defer
也是Go裏面比較特別的一個關鍵字了,主要就是用來保證在程序執行過程中,defer後面的函數都會被執行到,一般用來關閉連接、清理資源等。
1. 結構概覽
1.1. defer
type _defer struct {
siz int32 // 參數的大小
started bool // 是否執行過了
sp uintptr // sp at time of defer
pc uintptr
fn *funcval
_panic *_panic // defer中的panic
link *_defer // defer鏈表,函數執行流程中的defer,會通過 link這個 屬性進行串聯
}
1.2. panic
type _panic struct {
argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
arg interface{} // argument to panic
link *_panic // link to earlier panic
recovered bool // whether this panic is over
aborted bool // the panic was aborted
}
1.3. g
因爲 defer panic 都是綁定在 運行的g上的,所以這裏說明一下g中與 defer panic相關的屬性
type g struct {
_panic *_panic // panic組成的鏈表
_defer *_defer // defer組成的先進後出的鏈表,同棧
}
2. 源碼分析
2.1. main
最開始,還是通過go tool
來分析一下,底層是通過什麼函數來實現的吧
func main() {
defer func() {
recover()
}()
panic("error")
}
go build -gcflags=all="-N -l" main.gogo tool objdump -s "main.main" main
▶ go tool objdump -s "main\.main" main | grep CALL
main.go:4 0x4548d0 e81b00fdff CALL runtime.deferproc(SB)
main.go:7 0x4548f2 e8b90cfdff CALL runtime.gopanic(SB)
main.go:4 0x4548fa e88108fdff CALL runtime.deferreturn(SB)
main.go:3 0x454909 e85282ffff CALL runtime.morestack_noctxt(SB)
main.go:5 0x4549a6 e8d511fdff CALL runtime.gorecover(SB)
main.go:4 0x4549b5 e8a681ffff CALL runtime.morestack_noctxt(SB)
綜合反編譯結果可以看出,defer
關鍵字首先會調用 runtime.deferproc
定義一個延遲調用對象,然後再函數結束前,調用 runtime.deferreturn
來完成 defer
定義的函數的調用
panic
函數就會調用 runtime.gopanic
來實現相關的邏輯
recover
則調用 runtime.gorecover
來實現 recover 的功能
2.2. deferproc
根據 defer 關鍵字後面定義的函數 fn 以及 參數的size,來創建一個延遲執行的 函數,並將這個延遲函數,掛在到當前g的 _defer 的鏈表上
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()
// 獲取一個_defer對象, 並放入g._defer鏈表的頭部
d := newdefer(siz)
// 設置defer的fn pc sp等,後面調用
d.fn = fn
d.pc = callerpc
d.sp = sp
switch siz {
case 0:
// Do nothing.
case sys.PtrSize:
// _defer 後面的內存 存儲 argp的地址信息
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
// 如果不是指針類型的參數,把參數拷貝到 _defer 的後面的內存空間
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}
return0()
}
這個函數看起來比較簡答,通過newproc
獲取一個 _defer 的對象,並加入到當前g的 _defer 鏈表的頭部,然後再把參數或參數的指針拷貝到 獲取到的 _defer對象的 後面的內存空間
2.2.1. newdefer
newdefer
的作用是獲取一個_defer對象, 並推入 g._defer鏈表的頭部
func newdefer(siz int32) *_defer {
var d *_defer
// 根據 size 通過deferclass判斷應該分配的 sizeclass,就類似於 內存分配預先確定好幾個sizeclass,然後根據size確定sizeclass,找對應的緩存的內存塊
sc := deferclass(uintptr(siz))
gp := getg()
// 如果sizeclass在既定的sizeclass範圍內,去g綁定的p上找
if sc < uintptr(len(p{}.deferpool)) {
pp := gp.m.p.ptr()
if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
// 當前sizeclass的緩存數量==0,且不爲nil,從sched上獲取一批緩存
systemstack(func() {
lock(&sched.deferlock)
for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
d := sched.deferpool[sc]
sched.deferpool[sc] = d.link
d.link = nil
pp.deferpool[sc] = append(pp.deferpool[sc], d)
}
unlock(&sched.deferlock)
})
}
// 如果從sched獲取之後,sizeclass對應的緩存不爲空,分配
if n := len(pp.deferpool[sc]); n > 0 {
d = pp.deferpool[sc][n-1]
pp.deferpool[sc][n-1] = nil
pp.deferpool[sc] = pp.deferpool[sc][:n-1]
}
}
// p和sched都沒有找到 或者 沒有對應的sizeclass,直接分配
if d == nil {
// Allocate new defer+args.
systemstack(func() {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
})
}
d.siz = siz
// 插入到g._defer的鏈表頭
d.link = gp._defer
gp._defer = d
return d
}
根據size獲取sizeclass,對sizeclass進行分類緩存,這是內存分配時的思想
先去p上分配,然後批量從全局 sched上獲取到本地緩存,這種二級緩存的思想真的是遍佈在go源碼的各個部分啊
2.3. deferreturn
func deferreturn(arg0 uintptr) {
gp := getg()
// 獲取g defer鏈表的第一個defer,也是最後一個聲明的defer
d := gp._defer
// 沒有defer,就不需要幹什麼事了
if d == nil {
return
}
sp := getcallersp()
// 如果defer的sp與callersp不匹配,說明defer不對應,有可能是調用了其他棧幀的延遲函數
if d.sp != sp {
return
}
// 根據d.siz,把原先存儲的參數信息獲取並存儲到arg0裏面
switch d.siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
}
fn := d.fn
d.fn = nil
// defer用過了就釋放了,
gp._defer = d.link
freedefer(d)
// 跳轉到執行defer
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
2.3.1.freedefer
釋放defer用到的函數,應該跟調度器、內存分配的思想是一樣的
func freedefer(d *_defer) {
// 判斷defer的sizeclass
sc := deferclass(uintptr(d.siz))
// 超出既定的sizeclass範圍的話,就是直接分配的內存,那就不管了
if sc >= uintptr(len(p{}.deferpool)) {
return
}
pp := getg().m.p.ptr()
// p本地sizeclass對應的緩衝區滿了,批量轉移一半到全局sched
if len(pp.deferpool[sc]) == cap(pp.deferpool[sc]) {
// 使用g0來轉移
systemstack(func() {
var first, last *_defer
for len(pp.deferpool[sc]) > cap(pp.deferpool[sc])/2 {
n := len(pp.deferpool[sc])
d := pp.deferpool[sc][n-1]
pp.deferpool[sc][n-1] = nil
pp.deferpool[sc] = pp.deferpool[sc][:n-1]
// 先將需要轉移的那批defer對象串成一個鏈表
if first == nil {
first = d
} else {
last.link = d
}
last = d
}
lock(&sched.deferlock)
// 把這個鏈表放到sched.deferpool對應sizeclass的鏈表頭
last.link = sched.deferpool[sc]
sched.deferpool[sc] = first
unlock(&sched.deferlock)
})
}
// 清空當前要釋放的defer的屬性
d.siz = 0
d.started = false
d.sp = 0
d.pc = 0
d.link = nil
pp.deferpool[sc] = append(pp.deferpool[sc], d)
}
二級緩存的思想,在 深入理解Go-goroutine的實現及Scheduler分析, 深入理解go-channel和select的原理, 深入理解Go-垃圾回收機制 已經分析過了,就不再過多分析了
2.4. gopanic
func gopanic(e interface{}) {
gp := getg()
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
atomic.Xadd(&runningPanicDefers, 1)
// 依次執行 g._defer鏈表的defer對象
for {
d := gp._defer
if d == nil {
break
}
// 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.
// 正常情況下,defer執行完成之後都會被移除,既然這個defer沒有移除,原因只有兩種: 1. 這個defer裏面引發了panic 2. 這個defer裏面引發了 runtime.Goexit,但是這個defer已經執行過了,需要移除,如果引發這個defer沒有被移除是第一個原因,那麼這個panic也需要移除,因爲這個panic也執行過了,這裏給panic增加標誌位,以待後續移除
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
d.started = true
// Record the panic that is running the defer.
// If there is a new panic during the deferred call, that panic
// will find d in the list and will mark d._panic (this panic) aborted.
// 把當前的panic 綁定到這個defer上面,defer裏面有可能panic,這種情況下就會進入到 上面d.started 的邏輯裏面,然後把當前的panic終止掉,因爲已經執行過了
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
// 執行defer.fn
p.argp = unsafe.Pointer(getargp(0))
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")
}
// 解決defer與panic的綁定關係,因爲 defer函數已經執行完了,如果有panic或Goexit就不會執行到這裏了
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)
// panic被recover了,就不需要繼續panic了,繼續執行剩餘的代碼
if p.recovered {
atomic.Xadd(&runningPanicDefers, -1)
gp._panic = p.link
// Aborted panics are marked but remain on the g.panic list.
// Remove them from the list.
// 從panic鏈表中移除aborted的panic,下面解釋
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
// 調用recovery, 恢復當前g的調度執行
mcall(recovery)
throw("recovery failed") // mcall should not return
}
}
// 打印panic信息
preprintpanics(gp._panic)
// panic
fatalpanic(gp._panic) // should not return
*(*int)(nil) = 0 // not reached
}
這裏解釋一下 gp._panic.aborted
的作用,以下面爲例
func main() {
defer func() { // defer1
recover()
}()
panic1()
}
func panic1() {
defer func() { // defer2
panic("error1") // panic2
}()
panic("error") // panic1
}
- 當執行到
panic("error")
時g._defer鏈表: g._defer->defer2->defer1
g._panic鏈表:g._panic->panic1
- 當執行到
panic("error1")
時g._defer鏈表: g._defer->defer2->defer1
g._panic鏈表:g._panic->panic2->panic1
-
繼續執行到 defer1 函數內部,進行recover()
此時會去恢復 panic2 引起的 panic, panic2.recovered = true,應該順着g._panic鏈表繼續處理下一個panic了,但是我們可以發現
panic1
已經執行過了,這也就是下面的代碼的邏輯了,去掉已經執行過的panicfor gp._panic != nil && gp._panic.aborted { gp._panic = gp._panic.link }
panic的邏輯可以梳理一下:
程序在遇到panic的時候,就不再繼續執行下去了,先把當前panic
掛載到 g._panic
鏈表上,開始遍歷當前g的g._defer
鏈表,然後執行_defer
對象定義的函數等,如果 defer函數在調用過程中又發生了 panic,則又執行到了 gopanic
函數,最後,循環打印所有panic的信息,並退出當前g。然而,如果調用defer的過程中,遇到了recover,則繼續進行調度(mcall(recovery))。
2.4.1. recovery
恢復一個被panic的g,重新進入並繼續執行調度
func recovery(gp *g) {
// Info about defer passed in G struct.
sp := gp.sigcode0
pc := gp.sigcode1
// Make the deferproc for this d return again,
// this time returning 1. The calling function will
// jump to the standard return epilogue.
// 記錄defer返回的sp pc
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1
// 重新恢復執行調度
gogo(&gp.sched)
}
2.5. gorecover
gorecovery
僅僅只是設置了 g._panic.recovered
的標誌位
func gorecover(argp uintptr) interface{} {
gp := getg()
p := gp._panic
// 需要根據 argp的地址,判斷是否在defer函數中被調用
if p != nil && !p.recovered && argp == uintptr(p.argp) {
// 設置標誌位,上面gopanic中會對這個標誌位做判斷
p.recovered = true
return p.arg
}
return nil
}
2.6. goexit
我們還忽略了一個點,當我們手動調用 runtime.Goexit()
退出的時候,defer函數也會執行,我們分析一下這種情況
func Goexit() {
// Run all deferred functions for the current goroutine.
// This code is similar to gopanic, see that implementation
// for detailed comments.
gp := getg()
// 遍歷defer鏈表
for {
d := gp._defer
if d == nil {
break
}
// 如果 defer已經執行過了,與defer綁定的panic 終止掉
if d.started {
if d._panic != nil {
d._panic.aborted = true
d._panic = nil
}
d.fn = nil
// 從defer鏈表中移除
gp._defer = d.link
// 釋放defer
freedefer(d)
continue
}
// 調用defer內部函數
d.started = true
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
if gp._defer != d {
throw("bad defer entry in Goexit")
}
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
// Note: we ignore recovers here because Goexit isn't a panic
}
// 調用goexit0,清除當前g的屬性,重新進入調度
goexit1()
}
2.7. 圖示解析
源碼這一塊閱讀起來難度並不是很大,如果還有什麼疑惑,希望下面的一副動圖能解開你的疑惑
作圖作的略拙劣,見諒
步驟解析:
- L3: 生成一個defer1,放到g._defer鏈表上
- L11: 生成一個defer2,掛載到g._defer鏈表上
- L14: panic1 調用 gopanic,將當前panic放到g._panic鏈表上
- L14: 因爲panic1,從g._defer 鏈表頭部提取到defer2,開始執行
- L12: 執行defer2,又一個panic,掛載到g._panic鏈表上
- L12: 因爲panic2,從g._defer鏈表頭部提取到defer2,發現defer2已經執行過了移出鏈表,,且defer2是因爲panic1而觸發的,跳過defer2,並abort panic1
- L12: 繼續提取g._defer鏈表的下一個,提取到defer1
- L5: defer1 執行recover,recover掉panic2,移除鏈表,判斷下一個panic,即panic1,panic1已經被defer2 aborted掉了,移除panic1
- defer1 執行完了,移除defer1
3. 關聯文檔
- 二級緩存,sizeclass: 深入理解Go-垃圾回收機制
- gogo goexit0 調度: 深入理解Go-goroutine的實現及Scheduler分析
4. 參考文檔
- 《Go語言學習筆記》--雨痕