GO語言中defer實(shí)現(xiàn)原理的示例詳解
GO 中 defer的實(shí)現(xiàn)原理
我們來回顧一下上次的分享,分享了關(guān)于 通道的一些知識(shí)點(diǎn)
- 分享了 GO 中通道是什么
- 通道的底層數(shù)據(jù)結(jié)構(gòu)詳細(xì)解析
- 通道在GO源碼中是如何實(shí)現(xiàn)的
- Chan 讀寫的基本原理
- 關(guān)閉通道會(huì)出現(xiàn)哪些異常,panic
- select 的簡(jiǎn)單應(yīng)用
要是對(duì) chan 通道還有點(diǎn)興趣的話,歡迎查看文章 GO 中 Chan 實(shí)現(xiàn)原理分享
defer 是什么
咱們一起來看看 defer 是個(gè)啥
是 GO 中的一個(gè)關(guān)鍵字
這個(gè)關(guān)鍵字,我們一般用在釋放資源,在 return 前會(huì)調(diào)用他
如果程序中有多個(gè) defer ,defer 的調(diào)用順序是按照類似棧的方式,后進(jìn)先出 LIFO的 ,這里順便寫一下
棧

遵循后進(jìn)先出原則
后進(jìn)入棧的,先出棧
先進(jìn)入棧的,后出棧
隊(duì)列

遵循先進(jìn)先出 , 我們就可以想象一個(gè)單向的管道,從左邊進(jìn),右邊出
先進(jìn)來,先出去
后進(jìn)來,后出去,不準(zhǔn)插隊(duì)
defer 實(shí)現(xiàn)原理
咱們先拋出一個(gè)結(jié)論,先心里有點(diǎn)底:
代碼中聲明 defer的位置,編譯的時(shí)候會(huì)插入一個(gè)函數(shù)叫做 deferproc ,在該defer所在的函數(shù)前插入一個(gè)返回的函數(shù),不是return 哦,是deferreturn
具體的 defer 的實(shí)現(xiàn)原理是咋樣的,我們還是一樣的,來看看 defer的底層數(shù)據(jù)結(jié)構(gòu)是啥樣的 ,
在 src/runtime/runtime2.go 的 type _defer struct {結(jié)構(gòu)
// A _defer holds an entry on the list of deferred calls.
// If you add a field here, add code to clear it in freedefer and deferProcStack
// This struct must match the code in cmd/compile/internal/gc/reflect.go:deferstruct
// and cmd/compile/internal/gc/ssa.go:(*state).call.
// Some defers will be allocated on the stack and some on the heap.
// All defers are logically part of the stack, so write barriers to
// initialize them are not required. All defers must be manually scanned,
// and for heap defers, marked.
type _defer struct {
siz int32 // includes both arguments and results
started bool
heap bool
// openDefer indicates that this _defer is for a frame with open-coded
// defers. We have only one defer record for the entire frame (which may
// currently have 0, 1, or more defers active).
openDefer bool
sp uintptr // sp at time of defer
pc uintptr // pc at time of defer
fn *funcval // can be nil for open-coded defers
_panic *_panic // panic that is running defer
link *_defer
// If openDefer is true, the fields below record values about the stack
// frame and associated function that has the open-coded defer(s). sp
// above will be the sp for the frame, and pc will be address of the
// deferreturn call in the function.
fd unsafe.Pointer // funcdata for the function associated with the frame
varp uintptr // value of varp for the stack frame
// framepc is the current pc associated with the stack frame. Together,
// with sp above (which is the sp associated with the stack frame),
// framepc/sp can be used as pc/sp pair to continue a stack trace via
// gentraceback().
framepc uintptr
}_defer 持有延遲調(diào)用列表中的一個(gè)條目 ,我們來看看上述數(shù)據(jù)結(jié)構(gòu)的參數(shù)都是啥意思
| tag | 說明 |
|---|---|
| siz | defer函數(shù)的參數(shù)和結(jié)果的內(nèi)存大小 |
| fn | 需要被延遲執(zhí)行的函數(shù) |
| _panic | defer 的 panic 結(jié)構(gòu)體 |
| link | 同一個(gè)協(xié)程里面的defer 延遲函數(shù),會(huì)通過該指針連接在一起 |
| heap | 是否分配在堆上面 |
| openDefer | 是否經(jīng)過開放編碼優(yōu)化 |
| sp | 棧指針(一般會(huì)對(duì)應(yīng)到匯編) |
| pc | 程序計(jì)數(shù)器 |
defer 關(guān)鍵字后面必須是跟函數(shù),這一點(diǎn)咱們要記住哦
通過上述參數(shù)的描述,我們可以知道,defer的數(shù)據(jù)結(jié)構(gòu)和函數(shù)類似,也是有如下三個(gè)參數(shù):
- 棧指針 SP
- 程序計(jì)數(shù)器 PC
- 函數(shù)的地址
可是我們是不是也發(fā)現(xiàn)了,成員里面還有一個(gè)link,同一個(gè)協(xié)程里面的defer 延遲函數(shù),會(huì)通過該指針連接在一起
這個(gè)link指針,是指向的一個(gè)defer單鏈表的頭,每次咱們聲明一個(gè)defer的時(shí)候,就會(huì)將該defer的數(shù)據(jù)插入到這個(gè)單鏈表頭部的位置,
那么,執(zhí)行defer的時(shí)候,我們是不是就能猜到defer 是咋取得了不?
前面有說到defer是后進(jìn)先出的,這里當(dāng)然也是遵循這個(gè)道理,取defer進(jìn)行執(zhí)行的時(shí)候,是從單鏈表的頭開始去取的。
咱們來畫個(gè)圖形象一點(diǎn)
在協(xié)程A中聲明2個(gè)defer,先聲明 defer test1()

再聲明 defer test2()

可以看出后聲明的defer會(huì)插入到單鏈表的頭,先聲明的defer被排到后面去了
咱們?nèi)〉臅r(shí)候也是一直取頭下來執(zhí)行,直到單鏈表為空。
咱一起來看看defer 的具體實(shí)現(xiàn)

源碼文件在 src/runtime/panic.go 中,查看 函數(shù) deferproc
// Create a new deferred function fn with siz bytes of arguments.
// The compiler turns a defer statement into a call to this.
//go:nosplit
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
gp := getg()
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}
// the arguments of fn are in a perilous state. The stack map
// for deferproc does not describe them. So we can't let garbage
// collection or stack copying trigger until we've copied them out
// to somewhere safe. The memmove below does that.
// Until the copy completes, we can only call nosplit routines.
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()
d := newdefer(siz)
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
d.link = gp._defer
gp._defer = d
d.fn = fn
d.pc = callerpc
d.sp = sp
switch siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}
// 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.
}deferproc 的作用是
創(chuàng)建一個(gè)新的遞延函數(shù) fn,參數(shù)為 siz 字節(jié),編譯器將一個(gè)延遲語句轉(zhuǎn)換為對(duì)this的調(diào)用
getcallersp():
得到deferproc之前的rsp寄存器的值,實(shí)現(xiàn)的方式所有平臺(tái)都是一樣的
//go:noescape func getcallersp() uintptr // implemented as an intrinsic on all platforms
callerpc := getcallerpc():
此處得到 rsp之后,存儲(chǔ)在 callerpc 中 , 此處是為了調(diào)用 deferproc 的下一條指令
d := newdefer(siz):
d := newdefer(siz) 新建一個(gè)defer 的結(jié)構(gòu),后續(xù)的代碼是在給defer 這個(gè)結(jié)構(gòu)的成員賦值
咱看看 deferproc 的大體流程
- 獲取
deferproc之前的rsp寄存器的值 - 使用
newdefer分配一個(gè) _defer 結(jié)構(gòu)體對(duì)象,并且將他放到當(dāng)前的_defer鏈表的頭 - 初始化_defer 的相關(guān)成員參數(shù)
- return0
來我們看看 newdefer的源碼
源碼文件在 src/runtime/panic.go 中,查看函數(shù)newdefer
// Allocate a Defer, usually using per-P pool.
// Each defer must be released with freedefer. The defer is not
// added to any defer chain yet.
//
// This must not grow the stack because there may be a frame without
// stack map information when this is called.
//
//go:nosplit
func newdefer(siz int32) *_defer {
var d *_defer
sc := deferclass(uintptr(siz))
gp := getg()
if sc < uintptr(len(p{}.deferpool)) {
pp := gp.m.p.ptr()
if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
// Take the slow path on the system stack so
// we don't grow newdefer's stack.
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)
})
}
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]
}
}
if d == nil {
// Allocate new defer+args.
systemstack(func() {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
})
}
d.siz = siz
d.heap = true
return d
}newderfer 的作用:
通常使用per-P池,分配一個(gè)Defer
每個(gè)defer可以自由的釋放。當(dāng)前defer 也不會(huì)加入任何一個(gè) defer鏈條中
getg():
獲取當(dāng)前協(xié)程的結(jié)構(gòu)體指針
// getg returns the pointer to the current g. // The compiler rewrites calls to this function into instructions // that fetch the g directly (from TLS or from the dedicated register). func getg() *g
pp := gp.m.p.ptr():
拿到當(dāng)前工作線程里面的 P
然后拿到 從全局的對(duì)象池子中拿一部分對(duì)象給到P的池子里面
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)
}

點(diǎn)進(jìn)去看池子的數(shù)據(jù)結(jié)構(gòu),其實(shí)里面的成員也就是 咱們之前說到的 _defer指針
其中 sched.deferpool[sc] 是全局的池子,pp.deferpool[sc] 是本地的池子
mallocgc分配空間
上述操作若 d 沒有拿到值,那么就直接使用 mallocgc 重新分配,且設(shè)置好 對(duì)應(yīng)的成員 siz 和 heap
if d == nil {
// Allocate new defer+args.
systemstack(func() {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
})
}
d.siz = siz
d.heap = true
mallocgc 具體實(shí)現(xiàn)在 src/runtime/malloc.go 中,若感興趣的話,可以深入看看這一塊,今天咱們不重點(diǎn)說這個(gè)函數(shù)
// Allocate an object of size bytes.
// Small objects are allocated from the per-P cache's free lists.
// Large objects (> 32 kB) are allocated straight from the heap.
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {}
最后再來看看return0
最后再來看看 deferproc 函數(shù)中的 結(jié)果返回return0()
// return0 is a stub used to return 0 from deferproc. // It is called at the very end of deferproc to signal // the calling Go function that it should not jump // to deferreturn. // in asm_*.s func return0()
return0 是用于從deferproc返回0的存根
它在deferproc函數(shù)的最后被調(diào)用,用來通知調(diào)用Go的函數(shù)它不應(yīng)該跳轉(zhuǎn)到deferreturn。
在正常情況下 return0 正常返回 0
可是異常情況下 return0 函數(shù)會(huì)返回 1,此時(shí)GO 就會(huì)跳轉(zhuǎn)到執(zhí)行 deferreturn
簡(jiǎn)單說下 deferreturn
deferreturn的作用就是情況defer里面的鏈表,歸還相應(yīng)的緩沖區(qū),或者把對(duì)應(yīng)的空間讓GC回收調(diào)
GO 中 defer 的規(guī)則
上面分析了GO 中defer 的實(shí)現(xiàn)原理之后,咱們現(xiàn)在來了解一下 GO 中應(yīng)用defer 是需要遵守 3 個(gè)規(guī)則的,咱們來列一下:
defer后面跟的函數(shù),叫延遲函數(shù),函數(shù)中的參數(shù)在defer語句聲明的時(shí)候,就已經(jīng)確定下來了- 延遲函數(shù)的執(zhí)行時(shí)按照后進(jìn)先出來的,文章前面也多次說到過,這個(gè)印象應(yīng)該很深刻吧,先出現(xiàn)的
defer后執(zhí)行,后出現(xiàn)的defer先執(zhí)行 - 延遲函數(shù)可能會(huì)影響到整個(gè)函數(shù)的返回值
咱們還是要來解釋一下的,上面第 2 點(diǎn),應(yīng)該都好理解,上面的圖也表明了 執(zhí)行順序
第一點(diǎn)咱們來寫個(gè)小DEMO
延遲函數(shù)中的參數(shù)在defer語句聲明的時(shí)候,就已經(jīng)確定下來了
func main() {
num := 1
defer fmt.Println(num)
num++
return
}別猜了,運(yùn)行結(jié)果是 1,小伙伴們可以將代碼拷貝下來,自己運(yùn)行一波
第三點(diǎn)也來一個(gè)DEMO
延遲函數(shù)可能會(huì)影響到整個(gè)函數(shù)的返回值
func test3() (res int) {
defer func() {
res++
}()
return 1
}
func main() {
fmt.Println(test3())
return
}上述代碼,我們?cè)?test3函數(shù)中的返回值,我們提前命名好了,本來應(yīng)該是返回結(jié)果為 1
可是在return 這里,執(zhí)行順序這樣的
res = 1
res++
因此,結(jié)果就是 2
總結(jié)
- 分享了defer是什么
- 簡(jiǎn)單示意了棧和隊(duì)列
- defer的數(shù)據(jù)結(jié)構(gòu)和實(shí)現(xiàn)原理,具體的源碼展示
- GO中defer的 3 條規(guī)則
以上就是GO語言中defer實(shí)現(xiàn)原理的示例詳解的詳細(xì)內(nèi)容,更多關(guān)于GO語言defer的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于Golang實(shí)現(xiàn)Redis協(xié)議解析器
這篇文章主要為大家詳細(xì)介紹了如何通過GO語言編寫簡(jiǎn)單的Redis協(xié)議解析器,文中的示例代碼講解詳細(xì),對(duì)我們深入了解Go語言有一定的幫助,需要的可以參考一下2023-03-03
一個(gè)Pod調(diào)度失敗后重新觸發(fā)調(diào)度的所有情況分析
這篇文章主要為大家介紹了一個(gè)Pod調(diào)度失敗后重新觸發(fā)調(diào)度的所有情況分析詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04

