defer學(xué)習(xí)
很多現(xiàn)代的變成語言中都會(huì)有defer關(guān)鍵字,Go語言的defer會(huì)在當(dāng)前函數(shù)或是方法返回之前執(zhí)行傳入的函數(shù),它會(huì)經(jīng)常被用于
關(guān)閉文件描述符,關(guān)閉數(shù)據(jù)庫鏈接和解鎖資源。
作為一個(gè)編程語言中的關(guān)鍵字,defer 的實(shí)現(xiàn)一定是由編譯器和運(yùn)行時(shí)共同完成的,
不過在深入源碼分析它的實(shí)現(xiàn)之前我們還是需要了解 defer 關(guān)鍵字的常見使用場(chǎng)景以及使用時(shí)的注意事項(xiàng)。
- 使用defer的最常見的場(chǎng)景就是在函數(shù)調(diào)用結(jié)束的時(shí)候完成一些收尾工作,比如在defer中回滾數(shù)據(jù)庫
func createPost(db *gorm.DB) error{
tx := db.Begin()
defer tx.Rollback()
if err := tx.Create(&Post{Author: "Draveness"}).Error; err != nil{
return err
}
return tx.Commit().Error
}
現(xiàn)象
我們?cè)?Go 語言中使用 defer 時(shí)會(huì)遇到兩個(gè)比較常見的問題,這里會(huì)介紹具體的場(chǎng)景并分析這兩個(gè)現(xiàn)象背后的設(shè)計(jì)原理:
- defer 關(guān)鍵字的調(diào)用時(shí)機(jī)以及多次調(diào)用 defer 時(shí)執(zhí)行順序是如何確定的;
- defer 關(guān)鍵字使用傳值的方式傳遞參數(shù)時(shí)會(huì)進(jìn)行預(yù)計(jì)算,導(dǎo)致不符合預(yù)期的結(jié)果;
作用域
向 defer 關(guān)鍵字傳入的函數(shù)會(huì)在函數(shù)返回之前運(yùn)行。假設(shè)我們?cè)?for 循環(huán)中多次調(diào)用 defer 關(guān)鍵字:
defer的執(zhí)行順序是從后向前執(zhí)行,先定義后執(zhí)行
func main(){
for i:=0;i<5;i++{
defer fmt.Println(i)
}
}
打印結(jié)果為:4,3,2,1
defer 傳入的函數(shù)不是在退出代碼塊的作用域時(shí)執(zhí)行的,它只會(huì)在當(dāng)前函數(shù)和方法返回之前被調(diào)用。
func main(){
{
defer fmt.Println("defer runs")
fmt.Println("block ends")
}
fmt.Println("main ends")
}
執(zhí)行結(jié)果為:
block ends
main ends
defer runs
預(yù)計(jì)算參數(shù)
Go 語言中所有的函數(shù)調(diào)用都是傳值的,defer 雖然是關(guān)鍵字,但是也繼承了這個(gè)特性。假設(shè)我們想要計(jì)算 main 函數(shù)運(yùn)行的時(shí)間,可能會(huì)寫出以下的代碼
func main() {
startedAt := time.Now()
defer fmt.Println(time.Since(startedAt))
time.Sleep(time.Second)
}
執(zhí)行結(jié)果為0s
調(diào)用 defer 關(guān)鍵字會(huì)立刻對(duì)函數(shù)中引用的外部參數(shù)進(jìn)行拷貝,所以 time.Since(startedAt) 的結(jié)果不是在 main 函數(shù)退出之前計(jì)算的,
而是在 defer 關(guān)鍵字調(diào)用時(shí)計(jì)算的,最終導(dǎo)致上述代碼輸出 0s。解決這個(gè)方法只通過傳參數(shù)的方式就可以解決。
數(shù)據(jù)結(jié)構(gòu)
defer 關(guān)鍵字在 Go 語言源代碼中對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu):
type _defer struct {
siz int32
started bool
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer
}
runtime._defer 結(jié)構(gòu)體是延遲調(diào)用鏈表上的一個(gè)元素,所有結(jié)構(gòu)體都會(huì)通過link字段串鏈成鏈表
- siz 是參數(shù)和結(jié)果的內(nèi)存大小
- sp和pc分別代表?xiàng)V羔樅驼{(diào)用方的程序技術(shù)器
- fn是defer關(guān)鍵字中傳入的函數(shù)
- _panic 是觸發(fā)延遲調(diào)用的結(jié)構(gòu)體
編譯過程
defer 關(guān)鍵字在運(yùn)行期間會(huì)調(diào)用 runtime.deferproc 函數(shù),這個(gè)函數(shù)接收了參數(shù)的大小和閉包所在的地址兩個(gè)參數(shù)。
編譯器不僅將 defer 關(guān)鍵字都轉(zhuǎn)換成 runtime.deferproc 函數(shù),它還會(huì)通過以下三個(gè)步驟為所有調(diào)用
defer 的函數(shù)末尾插入 runtime.deferreturn 的函數(shù)調(diào)用:
- cmd/compile/internal/gc.walkstmt 在遇到 ODEFER 節(jié)點(diǎn)時(shí)會(huì)執(zhí)行 Curfn.Func.SetHasDefer(true) 設(shè)置當(dāng)前函數(shù)的 hasdefer;
- cmd/compile/internal/gc.buildssa 會(huì)執(zhí)行 s.hasdefer = fn.Func.HasDefer() 更新 state 的 hasdefer;
- cmd/compile/internal/gc.state.exit 會(huì)根據(jù) state 的 hasdefer 在函數(shù)返回之前插入 runtime.deferreturn 的函數(shù)調(diào)用;
編譯過程
defer 關(guān)鍵字的運(yùn)行時(shí)實(shí)現(xiàn)了分成兩部分
- runtime.deferproc 函數(shù)負(fù)責(zé)創(chuàng)建新的延遲調(diào)用
- runtime.deferreturn 函數(shù)負(fù)責(zé)在函數(shù)調(diào)用結(jié)束時(shí)執(zhí)行所有的延遲調(diào)用
創(chuàng)建延遲調(diào)用
runtime.deferproc 會(huì)為 defer 創(chuàng)建一個(gè)新的 runtime._defer 結(jié)構(gòu)體、設(shè)置它的函數(shù)指針 fn、
程序計(jì)數(shù)器 pc 和棧指針 sp 并將相關(guān)的參數(shù)拷貝到相鄰的內(nèi)存空間中:
func deferproc(siz int32,fn *funcval){
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.fn =fn
d.pc = callerpc
d.sp = sp
switch siz {
case 0:
case sys.PtrSize:
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
memmove(deferArgs(d),unsafe.Pointer(argp),uintptr(siz))
}
return0()
}
最后調(diào)用的 runtime.return0 函數(shù)的作用是避免無限遞歸調(diào)用 runtime.deferreturn,它是唯一一個(gè)不會(huì)觸發(fā)由延遲調(diào)用的函數(shù)了。
runtime.deferproc 中 runtime.newdefer 的作用就是想盡辦法獲得一個(gè) runtime._defer 結(jié)構(gòu)體,辦法總共有三個(gè):
- 從調(diào)度器的延遲調(diào)用緩存池 sched.deferpool 中取出結(jié)構(gòu)體并將該結(jié)構(gòu)體追加到當(dāng)前的Goroutine的緩存池中
- 從Goroutine的延遲調(diào)用緩存池的pp.deferpool中取出結(jié)構(gòu)體
- 通過runtime.mallocgc創(chuàng)建一個(gè)新的結(jié)構(gòu)體
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 {
for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
d := sched.deferpool[sc]
sched.deferpool[sc] = d.link
pp.deferpool[sc] = append(pp.deferpool[sc], d)
}
}
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 {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
}
d.siz = siz
d.link = gp._defer
gp._defer = d
return d
}
無論使用哪種方式獲取 runtime._defer,它都會(huì)被追加到所在的 Goroutine _defer 鏈表的最前面。
defer 關(guān)鍵字插入時(shí)是從后向前的,而 defer 關(guān)鍵字執(zhí)行是從前向后的,而這就是后調(diào)用的 defer 會(huì)優(yōu)先執(zhí)行的原因。
執(zhí)行延遲調(diào)用
runtime.deferreturn 函數(shù)會(huì)多次判斷當(dāng)前 Goroutine 的 _defer 鏈表中是否有未執(zhí)行的剩余結(jié)構(gòu),
在所有的延遲函數(shù)調(diào)用都執(zhí)行完成之后,該函數(shù)才會(huì)返回。
小結(jié)
defer 關(guān)鍵字的實(shí)現(xiàn)主要是依靠編譯器和運(yùn)行時(shí)的協(xié)作
- 編譯期
- 將defer 關(guān)鍵字被替換成runtime.deferproc
- 在調(diào)用defer關(guān)鍵字的函數(shù)返回之前插入runtime.deferreturn
- 運(yùn)行時(shí)
- runtime.deferproc 會(huì)將一個(gè)新的 runtime._defer 結(jié)構(gòu)體追加到當(dāng)前 Goroutine 的鏈表頭;
- runtime.deferreturn 會(huì)從 Goroutine 的鏈表中取出 runtime._defer 結(jié)構(gòu)并依次執(zhí)行;