Go語言之defer

定義

defer語句被用于預定對一個函數的調用。我們把這類被defer語句調用的函數稱為延遲函數。

注意,defer語句只能出現在函數或方法的內部。

一條defer語句總是以關鍵字defer開始。在defer的右邊還必會有一條表達式語句,且它們之間要以空格" "分隔,如:

defer fmt.Println("The finishing touches.")

這里的表達式語句必須代表一個函數或方法的調用。注意,既然是表達式語句,那么一些調用表達式就是不被允許出現在這里的。比如,針對各種內建函數的那些調用表達式。因為它們不能被稱為表達式語句。另外,在這個位置上出現的表達式語句是不能被圓括號括起來的。

defer語句的執行時機總是在直接包含它的那個函數把流程控制權交還給它的調用方的前一刻,無論defer語句出現在外圍函數的函數體中的哪一個位置上

具體分為下面幾種情況:

  • 當外圍函數的函數體中的相應語句全部被正常執行完畢的時候,只有在該函數中的所有defer語句都被執行完畢之后該函數才會真正地結束執行。
  • 當外圍函數的函數體中的return語句被執行的時候,只有在該函數中的所有defer語句都被執行完畢之后該函數才會真正地返回。
  • 當在外圍函數中有運行時恐慌發生的時候,只有在該函數中的所有defer語句都被執行完畢之后該運行時恐慌才會真正地被擴散至該函數的調用方。

總之,外圍函數的執行的結束會由于其中defer語句的執行而被推遲。

正因為defer語句有著這樣的特性,所以它成為了執行釋放資源或異常處理等收尾任務的首選。

defer優勢

使用defer語句的優勢有兩個:

  1. 收尾任務總會被執行,我們不會再因粗心大意而造成資源的浪費;

  2. 我們可以把它們放到外圍函數的函數體中的任何地方(一般是函數體開始處或緊跟在申請資源的語句的后面),而不是只能放在函數體的最后。這使得代碼邏輯變得更加清晰,并且收尾任務是否被合理的指定也變得一目了然。

在defer語句中,我們調用的函數不但可以是已聲明的命名函數,還可以是臨時編寫的匿名函數,就像這樣:

defer func() { 
       fmt.Println("The finishing touches.")    
}()

注意,一個針對匿名函數的調用表達式是由一個函數字面量和一個代表了調用操作的一對圓括號組成的。

我們在這里選擇匿名函數的好處是可以使該函數的收尾任務的內容更加直觀。不過,我們也可以把比較通用的收尾任務單獨放在一個命名函數中,然后再將其添加到需要它的defer語句中。無論在defer關鍵字右邊的是命名函數還是匿名函數,我們都可以稱之為延遲函數。因為它總是會被延遲到外圍函數執行結束前一刻才被真正的調用。
每當defer語句被執行的時候,傳遞給延遲函數的參數都會以通常的方式被求值。如下例:

func begin(funcName string) string {
    fmt.Printf("Enter function %s.\n", funcName)    
    return funcName
}
func end(funcName string) string {
    fmt.Printf("Exit function %s.\n", funcName)    
    return funcName
}
func record() {
    defer end(begin("record"))    
    fmt.Println("In function record.")
}

outputs:

  Enter function record.
  In function record.
  Exit function record.

示例中,調用表達式begin("record")是作為record函數的參數出現的。它會在defer語句被執行的時候被求值。也就是說,在record函數的函數體被執行之處,begin函數就被調用了。然而,end函數卻是在外圍函數record執行結束的前一刻被調用的。

這樣做除了可以避免參數值在延遲函數被真正調用之前再次發生改變而給該函數的執行造成影響之外,還是處于同一條defer語句可能會被多次執行的考慮。如下例:

func printNumbers() {
    for i := 0; i < 5; i++ {        
      defer fmt.Printf("%d ", i)    
    }
 }

outputs:

  4 3 2 1 0

在for語句的每次迭代的過程中都會執行一次其中的defer語句。在第一次迭代中,針對延遲函數的調用表達式最終會是fmt.Printf("%d", 0)。這是由于在defer語句被執行的時候,參數i先被求值為了0,隨后這個值被代入到了原來的調用表達式中,并形成了最終的延遲函數調用表達式。顯然,這時的調用表達式已經與原來的表達式有所不同了。所以,Go語言會把代入參數值之后的調用表達式另行存儲。以此類推,后面幾次迭代所產生的延遲函數調用表達式依次為:

fmt.Printf("%d ", 1)
fmt.Printf("%d ", 2)
fmt.Printf("%d ", 3)
fmt.Printf("%d ", 4)

defer語句執行順序

對延遲函數調用表達式的求值順序是與它們所在的defer語句被執行的順序完全相反的。每當Go語言把已代入參數值的延遲函數調用表達式另行存儲后,還會把它追加到一個專門為當前外圍函數存儲延遲函數調用表達式的列表中。而這個列表總是LIFO(Last In First Out,即后進先出)的。因此,這些延遲函數調用表達式的求值順序會是:

fmt.Printf("%d ", 4)
fmt.Printf("%d ", 3)
fmt.Printf("%d ", 2)
fmt.Printf("%d ", 1)
fmt.Printf("%d ", 0)

例:

func appendNumbers(ints []int) (result []int) {
    result = append(ints, 1)    
    fmt.Println(result)    
    defer func() {  
          result = append(result, 2)    
    }()
    result = append(result, 3)    
    fmt.Println(result)    defer func() { 
           result = append(result, 4)    
    }()
    result = append(result, 5)    
    fmt.Println(result)    defer func() {
            result = append(result, 6)    
    }()    
    return result
 }

outputs:

  [0 1 3 5 6 4 2]

例:

func printNumbers() {
    for i := 0; i < 5; i++ {     
       defer func() {      
             fmt.Printf("%d ", i)     
       }()
    }
}

outputs:

  5 5 5 5 5

在defer語句被執行的時候傳遞給延遲函數的參數都會被求值,但是延遲函數調用表達式并不會在那時被求值。當我們把
fmt.Printf("%d ", i)
改為

defer func() {
            fmt.Printf("%d ", i)
}()

之后,雖然變量i依然是有效的,但是它所代表的值卻已經完全不同了。在for語句的迭代過程中,其中defer語句被執行了5次。但是,由于我們并沒有給延遲函數傳遞任何參數,所以Go語言運行時系統也就不需要對任何作為延遲函數的參數值的表達式進行求值(因為它們根本不存在)。在for語句被執行完畢的時候,共有5個延遲函數調用表達式被存儲到了它們的專屬列表中。注意,被存儲在專屬列表中的是5個相同的調用表達式:

defer func() {
            fmt.Printf("%d ", i)
}()

在printNumbers函數的執行即將結束的時候,那個專屬列表中的延遲函數調用表達式就會被逆序的取出并被逐個的求值。然而,這時的變量i已經被修改為了5。因此,對5個相同的調用表達式的求值都會使標準輸出上打印出5.
  如何修正這個問題呢?
  將defer語句修改為:

defer func(i int) {
            fmt.Printf("%d ", i)        
}(i)

我們雖然還是以匿名函數作為延遲函數,但是卻為這個匿名函數添加了一個參數聲明,并在代表調用操作的圓括號中加入了作為參數的變量i。這樣,在defer語句被執行的時候,傳遞給延遲函數的這個參數i就會被求值。最終的延遲函數調用表達式也會類似于:

defer func(i int) {
            fmt.Printf("%d ", i)        
}(0)

又因為延遲函數聲明中的參數i屏蔽了在for語句中聲明的變量i,所以在延遲函數被執行的時候,其中那條打印語句中所使用的i值即為傳遞給延遲函數的那個參數值。

如果延遲函數是一個匿名函數,并且在外圍函數的聲明中存在命名的結果聲明,那么在延遲函數中的代碼是可以對命名結果的值進行訪問和修改的。如下例:

func modify(n int) (number int) {
    fmt.Println(number)    
    defer func() { 
           number += n    
    }()
    number++    
    return
}
  • modify(2),結果為:3

雖然在延遲函數的聲明中可以包含結果聲明,但是其返回的結果值會在它被執行完畢時丟棄。因此,作為慣例,我們在編寫延遲函數的聲明的時候不會為其添加結果聲明。另一方面,推薦以傳參的方式提供延遲函數所需的外部值。如下例:

func modify(n int) (number int) {
    fmt.Println(number)   
     defer func(plus int) (result int) {
             result = n + plus        
             number += result        
             return    
    }(3)
    number++    
    return
}
  • modify(2),結果為:6

我們可以把想要傳遞給延遲函數的參數值依照規則放入到那個代表調用操作的圓括號中,就像調用普通函數那樣。另一方面,雖然我們在延遲函數的函數體中返回了結果值,但是卻不會產生任何效果。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,565評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,115評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,577評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,514評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,234評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,621評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,641評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,822評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,380評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,128評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,319評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,879評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,548評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,970評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,229評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,048評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,285評論 2 376

推薦閱讀更多精彩內容

  • 86.復合 Cases 共享相同代碼塊的多個switch 分支 分支可以合并, 寫在分支后用逗號分開。如果任何模式...
    無灃閱讀 1,405評論 1 5
  • 官網 中文版本 好的網站 Content-type: text/htmlBASH Section: User ...
    不排版閱讀 4,427評論 0 5
  • Lua 5.1 參考手冊 by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 13,870評論 0 38
  • 丫頭真是古靈精怪,無敵可愛。 一歲四個月的丫頭已經懂事了。吃完早餐我叫老板買單,老板太忙沒聽見,寶寶拿著錢就走向老...
    菜鳥快跑閱讀 309評論 0 4
  • 每件事,如果你能盡全力去完成,可以無愧的說一聲:已盡人事。那一定會有意外的收獲。比如論文這件事。 一直以來,對論文...
    小餅子的記事本閱讀 556評論 0 1