一娜睛、什么是defer饲齐?
defer是Go語言提供的一種用于注冊(cè)延遲調(diào)用的機(jī)制:讓函數(shù)或語句可以在當(dāng)前函數(shù)執(zhí)行完畢后(包括通過return正常結(jié)束或者panic導(dǎo)致的異常結(jié)束)執(zhí)行。
defer語句通常用于一些成對(duì)操作的場(chǎng)景:打開連接/關(guān)閉連接融痛;加鎖/釋放鎖掺冠;打開文件/關(guān)閉文件等。
defer在一些需要回收資源的場(chǎng)景非常有用汰扭,可以很方便地在函數(shù)結(jié)束前做一些清理操作。在打開資源語句的下一行福铅,直接一句defer就可以在函數(shù)返回前關(guān)閉資源萝毛,可謂相當(dāng)優(yōu)雅。
f, _ := os.Open("defer.txt")
defer f.Close()
注意:以上代碼滑黔,忽略了err, 實(shí)際上應(yīng)該先判斷是否出錯(cuò)珊泳,如果出錯(cuò)了鲁冯,直接return. 接著再判斷f是否為空,如果f為空色查,就不能調(diào)用f.Close()函數(shù)了,會(huì)直接panic的撞芍。
二秧了、為什么需要defer?
程序員在編程的時(shí)候序无,經(jīng)常需要打開一些資源验毡,比如數(shù)據(jù)庫連接、文件帝嗡、鎖等晶通,這些資源需要在用完之后釋放掉,否則會(huì)造成內(nèi)存泄漏哟玷。
但是程序員都是人狮辽,是人就會(huì)犯錯(cuò)。因此經(jīng)常有程序員忘記關(guān)閉這些資源巢寡。Golang直接在語言層面提供defer關(guān)鍵字喉脖,在打開資源語句的下一行,就可以直接用defer語句來注冊(cè)函數(shù)結(jié)束后執(zhí)行關(guān)閉資源的操作抑月。因?yàn)檫@樣一顆“小小”的語法糖树叽,程序員忘寫關(guān)閉資源語句的情況就大大地減少了。
三谦絮、怎樣合理使用defer?
defer的使用其實(shí)非常簡(jiǎn)單:
f,err := os.Open("test.txt")
defer func() {
if f != nil {
f.Close()
}
}()
if err != nil {
panic(err)
}
在打開文件的語句附近题诵,用defer語句關(guān)閉文件。這樣层皱,在函數(shù)結(jié)束之前性锭,會(huì)自動(dòng)執(zhí)行defer后面的語句來關(guān)閉文件。
當(dāng)然奶甘,defer會(huì)有小小地延遲篷店,對(duì)時(shí)間要求特別特別特別高的程序,可以避免使用它臭家,其他一般忽略它帶來的延遲疲陕。
四、defer進(jìn)階
4.1 defer的底層原理是什么钉赁?
我們先看一下官方對(duì)defer的解釋:
Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred. If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the “defer” statement is executed.
翻譯一下:每次defer語句執(zhí)行的時(shí)候蹄殃,會(huì)把函數(shù)“壓棧”你踩,函數(shù)參數(shù)會(huì)被拷貝下來诅岩;當(dāng)外層函數(shù)(非代碼塊讳苦,如一個(gè)for循環(huán))退出時(shí),defer函數(shù)按照定義的逆序執(zhí)行吩谦;如果defer執(zhí)行的函數(shù)為nil, 那么會(huì)在最終調(diào)用函數(shù)的產(chǎn)生panic.
defer語句并不會(huì)馬上執(zhí)行鸳谜,而是會(huì)進(jìn)入一個(gè)棧,函數(shù)return前式廷,會(huì)按先進(jìn)后出的順序執(zhí)行咐扭。也說是說最先被定義的defer語句最后執(zhí)行。先進(jìn)后出的原因是后面定義的函數(shù)可能會(huì)依賴前面的資源滑废,自然要先執(zhí)行蝗肪;否則,如果前面先執(zhí)行蠕趁,那后面函數(shù)的依賴就沒有了薛闪。
在defer函數(shù)定義時(shí),對(duì)外部變量的引用是有兩種方式的俺陋,分別是作為函數(shù)參數(shù)和作為閉包引用豁延。
- 作為函數(shù)參數(shù),則在defer定義時(shí)就把值傳遞給defer倔韭,并被cache起來术浪;
- 作為閉包引用,則會(huì)在defer函數(shù)真正調(diào)用時(shí)根據(jù)整個(gè)上下文確定當(dāng)前的值寿酌。
defer后面的語句在執(zhí)行的時(shí)候胰苏,函數(shù)調(diào)用的參數(shù)會(huì)被保存起來,也就是復(fù)制了一份醇疼。真正執(zhí)行的時(shí)候硕并,實(shí)際上用到的是這個(gè)復(fù)制的變量,因此如果此變量是一個(gè)“值”秧荆,那么就和定義的時(shí)候是一致的倔毙。如果此變量是一個(gè)“引用”,那么就可能和定義的時(shí)候不一致乙濒。
func main() {
var whatever [3]struct{}
for i := range whatever {
defer func() {
fmt.Println(i)
}()
}
}
執(zhí)行結(jié)果:
2
2
2
defer后面跟的是一個(gè)閉包陕赃,i是“引用”類型的變量,最后i的值為2, 因此最后打印了三個(gè)2.
有了上面的基礎(chǔ)颁股,我們來檢驗(yàn)一下成果:
type number int
func (n number) print() { fmt.Println(n) }
func (n *number) pprint() { fmt.Println(*n) }
func main() {
var n number
defer n.print()
defer n.pprint()
defer func() { n.print() }()
defer func() { n.pprint() }()
n = 3
}
執(zhí)行結(jié)果是:
3
3
3
0
第四個(gè)defer語句是閉包么库,引用外部函數(shù)的n, 最終結(jié)果是3;
第三個(gè)defer語句同第四個(gè);
第二個(gè)defer語句甘有,n是引用诉儒,最終求值是3.
第一個(gè)defer語句,對(duì)n直接求值亏掀,開始的時(shí)候n=0, 所以最后是0;
4.2 defer命令的拆解
以一條語句為例:
return xxx
上面這條語句經(jīng)過編譯之后忱反,變成了三條指令:
1. 返回值 = xxx
2. 調(diào)用defer函數(shù)
3. 空的return
1,3步才是Return 語句真正的命令泛释,第2步是defer定義的語句,這里可能會(huì)操作返回值温算。
下面我們來看兩個(gè)例子怜校,試著將return語句和defer語句拆解到正確的順序。
第一個(gè)例子:
func f() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
拆解后:
func f() (r int) {
t := 5
// 1. 賦值指令
r = t
// 2. defer被插入到賦值與返回之間執(zhí)行米者,這個(gè)例子中返回值r沒被修改過
func() {
t = t + 5
}
// 3. 空的return指令
return
}
這里第二步?jīng)]有操作返回值r, 因此韭畸,main函數(shù)中調(diào)用f()得到5.
第二個(gè)例子:
func f() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}
拆解后:
func f() (r int) {
// 1. 賦值
r = 1
// 2. 這里改的r是之前傳值傳進(jìn)去的r,不會(huì)改變要返回的那個(gè)r值
func(r int) {
r = r + 5
}(r)
// 3. 空的return
return
}
因此蔓搞,main函數(shù)中調(diào)用f()得到1.
4.3 defer語句的參數(shù)
defer語句表達(dá)式的值在定義時(shí)就已經(jīng)確定了。下面展示三個(gè)函數(shù):
func f1() {
var err error
defer fmt.Println(err)
err = errors.New("defer error")
return
}
func f2() {
var err error
defer func() {
fmt.Println(err)
}()
err = errors.New("defer error")
return
}
func f3() {
var err error
defer func(err error) {
fmt.Println(err)
}(err)
err = errors.New("defer error")
return
}
func main() {
f1()
f2()
f3()
}
運(yùn)行結(jié)果:
<nil>
defer error
<nil>
第1随橘,3個(gè)函數(shù)是因?yàn)樽鳛楹瘮?shù)參數(shù)喂分,定義的時(shí)候就會(huì)求值,定義的時(shí)候err變量的值都是nil, 所以最后打印的時(shí)候都是nil. 第2個(gè)函數(shù)的參數(shù)其實(shí)也是會(huì)在定義的時(shí)候求值机蔗,只不過蒲祈,第2個(gè)例子中是一個(gè)閉包,它引用的變量err在執(zhí)行的時(shí)候最終變成defer error了萝嘁。關(guān)于閉包在本文后面有介紹梆掸。
第3個(gè)函數(shù)的錯(cuò)誤還比較容易犯,在生產(chǎn)環(huán)境中牙言,很容易寫出這樣的錯(cuò)誤代碼酸钦。最后defer語句沒有起到作用。
4.4 defer配合recover
panic會(huì)停掉當(dāng)前正在執(zhí)行的程序咱枉,不只是當(dāng)前協(xié)程卑硫。在這之前,它會(huì)有序地執(zhí)行完當(dāng)前協(xié)程defer列表里的語句蚕断,其它協(xié)程里掛的defer語句不作保證欢伏。因此,我們經(jīng)常在defer里掛一個(gè)recover語句亿乳,防止程序直接掛掉硝拧,這起到了try...catch的效果。
注意葛假,recover()函數(shù)只在defer的上下文中才有效(且只有通過在defer中用匿名函數(shù)調(diào)用才有效)障陶,直接調(diào)用的話,只會(huì)返回nil.
func main() {
defer fmt.Println("defer main")
var user = os.Getenv("USER_")
go func() {
defer func() {
fmt.Println("defer caller")
if err := recover(); err != nil {
fmt.Println("recover success. err: ", err)
}
}()
func() {
defer func() {
fmt.Println("defer here")
}()
if user == "" {
panic("should set user env.")
}
// 此處不會(huì)執(zhí)行
fmt.Println("after panic")
}()
}()
time.Sleep(100)
fmt.Println("end of main function")
}
panic最終會(huì)被recover捕獲到桐款,穩(wěn)住主進(jìn)程咸这,不影響整體服務(wù)。