1. 程式人生 > 其它 >Golang 中的 defer 關鍵字

Golang 中的 defer 關鍵字

0x00 defer 是啥

用一段簡單的程式碼演示

package main

import (
	"fmt"
)

func main() {
    defer fmt.Println("this defer fmt!!")
    fmt.Println("this is normal fmt!!")
}

以上程式碼的輸出如下:

defer 的作用就是註冊一個方法的呼叫,該方法將在程式返回後進行輸出。順帶,defer 的執行佇列是一個連結串列,當使用一次 defer ,就會在連結串列的頭結點插入一個執行結點,然後函式返回執行 defer 註冊的操作時會依次取頭結點執行,所以最後呈現的效果就是 “先入後出” 的執行順序。

0x01 實誠的建議

defer 很好用,特別是用來關閉一切需要顯式關閉的物件時,比如 file 的讀寫物件,golang 裡協程的計數器 sync.WaitGroup ,在建立了一個類似的物件後為其註冊一個 defer *.close() 呼叫絕對是一個很明智的選擇。

除此之外不建議用 defer,如果用 defer 來計算函式返回某些數值的話,往往會得到預期之外的結果。舉個栗子:

func defer_test1(){
    a, b := 1, 2
    defer add(a, b)
    a++
    b++
}

func add (a,b int){
    fmt.Println(a + b)
}

上述程式碼將輸出 3 而不是使用 defer 設想的 5,出現這個問題的原因是因為 golang 中 defer 底層仍然採用的是值傳遞,當我們使用 defer 關鍵字時,它會向 defer 的執行佇列裡面拷貝引用的外層引數,比如上面這串程式碼中的 add 方法就是外層引數,那麼內層引數 golang 又會怎麼處理呢?上面說到 defer 的函式呼叫也是值傳遞的,它會將內層引數直接計算出來然後傳遞給外層引數,也就是 a = 1, b = 2 ,所以計算出來是 3

這個時候就肯定有人想,既然是因為值傳遞引起的這個問題,那麼我強制使用指標傳遞引用會發生什麼呢?結果顯而易見,引用傳遞的值將得到預期的 5

func defer_test1(){
    a, b := 1, 2
    defer add(&a, &b)
    a++
    b++
}

func add (a,b *int){
    fmt.Println(*a + *b)
}

但是,你以為這就結束了嘛 -~-,當然沒有!如果吧程式碼改成下面這樣呢

func defer_test1(){
    a, b := 1, 2
    defer fmt.Println(add(&a, &b))
    a++
    b++
}

func add (a,b *int) int{
    fmt.Println(*a + *b)
    return *a + *b
}

func add2(a, b int) int{
    return a + b
}

會輸出 3 還是 5 ?答案是 3

對於這個輸出的理解,我是這樣子理解的:對於 defer 來說外層引數為 fmt.Println 內層引數為 add 這裡會拷貝 add 的值,也就是計算後的返回值 3。再寫個程式碼檢驗一下。

func defer_test1(){
	a, b := 1, 2
	defer add2(add(&a, &b), add(&a, &b))
	a++
	b++
}

func add (a,b *int) int{
    fmt.Println(*a + *b)
    return *a + *b
}

func add2(a, b int) int{
    return a + b
}

預期結果是 6 ,實際輸出跟預期一致

0x02 不需要建議

那如果非要在結束時計算呢?我們可以使用匿名函式的方式來達到效果。

func defer_test1(){
	a, b := 1, 2
	defer func (){
		fmt.Println(a + b)
	}()
	a++
	b++
}

輸出:

如上,就能夠達到我們的目的。

鳴謝

Go 語言設計與實現》第 5.3 節