定義
defer語句被用于預定對一個函數的調用。我們把這類被defer語句調用的函數稱為延遲函數。
注意,defer語句只能出現在函數或方法的內部。
一條defer語句總是以關鍵字defer開始。在defer的右邊還必會有一條表達式語句,且它們之間要以空格" "分隔,如:
defer fmt.Println("The finishing touches.")
這里的表達式語句必須代表一個函數或方法的調用。注意,既然是表達式語句,那么一些調用表達式就是不被允許出現在這里的。比如,針對各種內建函數的那些調用表達式。因為它們不能被稱為表達式語句。另外,在這個位置上出現的表達式語句是不能被圓括號括起來的。
defer語句的執行時機總是在直接包含它的那個函數把流程控制權交還給它的調用方的前一刻,無論defer語句出現在外圍函數的函數體中的哪一個位置上。
具體分為下面幾種情況:
- 當外圍函數的函數體中的相應語句全部被正常執行完畢的時候,只有在該函數中的所有defer語句都被執行完畢之后該函數才會真正地結束執行。
- 當外圍函數的函數體中的return語句被執行的時候,只有在該函數中的所有defer語句都被執行完畢之后該函數才會真正地返回。
- 當在外圍函數中有運行時恐慌發生的時候,只有在該函數中的所有defer語句都被執行完畢之后該運行時恐慌才會真正地被擴散至該函數的調用方。
總之,外圍函數的執行的結束會由于其中defer語句的執行而被推遲。
正因為defer語句有著這樣的特性,所以它成為了執行釋放資源或異常處理等收尾任務的首選。
defer優勢
使用defer語句的優勢有兩個:
收尾任務總會被執行,我們不會再因粗心大意而造成資源的浪費;
我們可以把它們放到外圍函數的函數體中的任何地方(一般是函數體開始處或緊跟在申請資源的語句的后面),而不是只能放在函數體的最后。這使得代碼邏輯變得更加清晰,并且收尾任務是否被合理的指定也變得一目了然。
在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
我們可以把想要傳遞給延遲函數的參數值依照規則放入到那個代表調用操作的圓括號中,就像調用普通函數那樣。另一方面,雖然我們在延遲函數的函數體中返回了結果值,但是卻不會產生任何效果。