Golang的關鍵字defer的使用方法
核心思想
在defer出現(xiàn)的地方插入了指令CALL runtime.deferproc,在函數(shù)返回的地方插入了CALL runtime.deferreturn。goroutine的控制結構中,有一張表記錄defer,調用runtime.deferproc時會將需要defer的表達式記錄在表中,而在調用runtime.deferreturn的時候,則會依次從defer表中“出棧”并執(zhí)行
如果有多個defer,調用順序類似棧,越后面的defer表達式越先被調用
defer鏈
defer信息會注冊到鏈表,當前執(zhí)行的 goroutine 持有這個鏈表的頭指針,每個 goroutine 都有一個對應的結構體struct G,其中有一個字段指向這個defer鏈表頭
type g struct {
// Stack parameters.
// stack describes the actual stack memory: [stack.lo, stack.hi).
// stackguard0 is the stack pointer compared in the Go stack growth prologue.
// It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
// stackguard1 is the stack pointer compared in the C stack growth prologue.
// It is stack.lo+StackGuard on g0 and gsignal stacks.
// It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
stack stack // offset known to runtime/cgo
stackguard0 uintptr // offset known to liblink
stackguard1 uintptr // offset known to liblink
_panic *_panic // innermost panic - offset known to liblink
// _defer 這個字段指向defer鏈表頭
_defer *_defer // innermost defer
...
}新注冊的defer會添加到鏈表頭,所以感覺像是棧那樣先進后出的調用:

源碼分析
deferproc一共有兩個參數(shù),第一個是參數(shù)和返回值的大小,第二個是指向funcval的指針
// 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
// 獲取當前goroutine
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()
// 通過偏移獲得參數(shù)
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()
// 創(chuàng)建defer結構體
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.
}// 以下是_defer結構體
// 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 記錄defer的參數(shù)和返回值共占多少字節(jié)
// 會直接分配在_defer后面,在注冊時保存參數(shù),在執(zhí)行完成時拷貝到調用者參數(shù)和返回值空間
siz int32 // includes both arguments and results
// started 標記是否已經(jīng)執(zhí)行
started bool
// heap go1.13優(yōu)化,標識是否為堆分配
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 是否是open defer,通過這些信息可以找到未注冊到鏈表的defer函數(shù)
openDefer bool
// sp 記錄調用者棧指針,可以通過它判斷自己注冊的defer是否已經(jīng)執(zhí)行完了
sp uintptr // sp at time of defer
// pc deferproc的返回地址
pc uintptr // pc at time of defer
// fn 要注冊的funcval
fn *funcval // can be nil for open-coded defers
// _panic 指向當前的panic,表示這個defer是由這個panic觸發(fā)的
_panic *_panic // panic that is running defer
// link 鏈到前一個注冊的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.
// 通過這些信息可以找到未注冊到鏈表的defer函數(shù)
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將參數(shù)注冊的時候拷貝到堆上,執(zhí)行時再(將參數(shù)和返回值)拷貝回棧上
go會分配不同規(guī)格的_defer pool,執(zhí)行時從空閑_defer中取一個出來用,沒有合適的再進行堆分配。用完以后再放回空閑_defer pool。以避免頻繁的堆分配和回收

優(yōu)化
go1.12中defer存在的問題:
- defer信息主要存儲在堆上,要在堆和棧上來回拷貝返回值和參數(shù)很慢
- defer結構體通過鏈表鏈起來,而鏈表的操作也很慢
go1.13中defer的優(yōu)化:
- 減少了defer信息的堆分配。再通過deferprocStack將整個defer注冊到defer鏈表中
- 將一般情況的defer信息存儲在函數(shù)棧幀的局部變量區(qū)域
- 顯示循環(huán)或者是隱式循環(huán)的defer還是需要用到go1.12中defer信息的堆分配
- 官方給出的性能提升是30%
go1.14中defer的優(yōu)化:
- 在編譯階段插入代碼,把defer函數(shù)的執(zhí)行邏輯展開在所屬函數(shù)內,避免創(chuàng)建defer結構體,而且不需要注冊到defer鏈表。稱為 open coded defer
- 與1.13一樣不適用于循環(huán)中的defer
- 性能幾乎提升了一個數(shù)量級
- open coded defer 中發(fā)生panic 或 調用runtime.Goexit(),后面未注冊到的defer函數(shù)無法執(zhí)行到,需要棧掃描。defer結構體中就多添加了一些字段,借助這些字段可以找到未注冊到鏈表中的defer函數(shù)
結果就是defer變快了,但是panic變慢了
defer添加了局部變量去判斷是否需要執(zhí)行,需要執(zhí)行的話就將標識df對應的位上或一下,如果是有條件的defer,需要根據(jù)具體條件去或df

deferprocStack
// deferprocStack queues a new deferred function with a defer record on the stack.
// The defer record must have its siz and fn fields initialized.
// All other fields can contain junk.
// The defer record must be immediately followed in memory by
// the arguments of the defer.
// Nosplit because the arguments on the stack won't be scanned
// until the defer record is spliced into the gp._defer list.
//go:nosplit
func deferprocStack(d *_defer) {
// 獲得當前 goroutine
gp := getg()
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}
// siz and fn are already set.
// The other fields are junk on entry to deferprocStack and
// are initialized here.
// 初始化 _defer 信息
d.started = false
d.heap = false
d.openDefer = false
d.sp = getcallersp()
d.pc = getcallerpc()
d.framepc = 0
d.varp = 0
// The lines below implement:
// d.panic = nil
// d.fd = nil
// d.link = gp._defer
// gp._defer = d
// But without write barriers. The first three are writes to
// the stack so they don't need a write barrier, and furthermore
// are to uninitialized memory, so they must not use a write barrier.
// The fourth write does not require a write barrier because we
// explicitly mark all the defer structures, so we don't need to
// keep track of pointers to them with a write barrier.
*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
*(*uintptr)(unsafe.Pointer(&d.fd)) = 0
*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
return0()
// No code can go here - the C return register has
// been set and must not be clobbered.
}到此這篇關于Golang的關鍵字defer的使用的文章就介紹到這了,更多相關 Golang關鍵字defer內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
go語言實現(xiàn)sftp包上傳文件和文件夾到遠程服務器操作
這篇文章主要介紹了go語言實現(xiàn)sftp包上傳文件和文件夾到遠程服務器操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12
Apache?IoTDB開發(fā)系統(tǒng)之Go原生接口方法
這篇文章主要為大家介紹了?Apache?IoTDB開發(fā)系統(tǒng)之Go原生接口方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-09-09

