go defer溫柔陷阱

一娜睛、什么是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ù)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末魔眨,一起剝皮案震驚了整個(gè)濱河市媳维,隨后出現(xiàn)的幾起案子酿雪,更是在濱河造成了極大的恐慌,老刑警劉巖侄刽,帶你破解...
    沈念sama閱讀 212,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件指黎,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡州丹,警方通過查閱死者的電腦和手機(jī)醋安,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來墓毒,“玉大人吓揪,你說我怎么就攤上這事∷疲” “怎么了柠辞?”我有些...
    開封第一講書人閱讀 158,369評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)主胧。 經(jīng)常有香客問我叭首,道長(zhǎng),這世上最難降的妖魔是什么踪栋? 我笑而不...
    開封第一講書人閱讀 56,799評(píng)論 1 285
  • 正文 為了忘掉前任焙格,我火速辦了婚禮,結(jié)果婚禮上夷都,老公的妹妹穿的比我還像新娘眷唉。我一直安慰自己,他們只是感情好损肛,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,910評(píng)論 6 386
  • 文/花漫 我一把揭開白布厢破。 她就那樣靜靜地躺著,像睡著了一般治拿。 火紅的嫁衣襯著肌膚如雪摩泪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,096評(píng)論 1 291
  • 那天劫谅,我揣著相機(jī)與錄音见坑,去河邊找鬼。 笑死捏检,一個(gè)胖子當(dāng)著我的面吹牛荞驴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播贯城,決...
    沈念sama閱讀 39,159評(píng)論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼熊楼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了能犯?” 一聲冷哼從身側(cè)響起鲫骗,我...
    開封第一講書人閱讀 37,917評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤犬耻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后执泰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體枕磁,經(jīng)...
    沈念sama閱讀 44,360評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,673評(píng)論 2 327
  • 正文 我和宋清朗相戀三年术吝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了计济。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,814評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡排苍,死狀恐怖沦寂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情淘衙,我是刑警寧澤凑队,帶...
    沈念sama閱讀 34,509評(píng)論 4 334
  • 正文 年R本政府宣布,位于F島的核電站幔翰,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏西壮。R本人自食惡果不足惜遗增,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,156評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望款青。 院中可真熱鬧做修,春花似錦、人聲如沸抡草。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽康震。三九已至燎含,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間腿短,已是汗流浹背屏箍。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評(píng)論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留橘忱,地道東北人赴魁。 一個(gè)月前我還...
    沈念sama閱讀 46,641評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像钝诚,于是被迫代替她去往敵國和親颖御。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,728評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容