Golang研學:defer!如何掌握並用好(延遲執行)

ZetaChow曉程式碼發表於2019-05-03

golang defer

defer:在函式A內用defer關鍵字呼叫的函式B會在在函式A return後執行。

先看一個基礎的例子,瞭解一下defer的效果

func main() {
	fmt.Println("in main func:", foo())
}

func foo() int {
	i := 0
	defer fmt.Println("in defer :", i)
	i = 1000
	fmt.Println("in foo:", i)
	return i+24
}
複製程式碼

這段程式碼執行後會列印出

in foo: 1000
in defer : 0
in main func: 1024
複製程式碼

變數i初始化為0defer指定fmt.Println函式延遲到return後執行,最後main函式呼叫foo列印返回值。

有什麼用途?

函式中會申明使用很多變數資源,函式結束時,我們通常會對它們做一些處理:銷燬、釋放(例如資料庫連結、檔案控制程式碼、流)。

一般情況下,我們會在return語句之前處理這些事情。

但是,如果函式中包含多個return,這些處理我們需要在每個return之前都操作一次,實際工作中經常出現遺漏,程式碼維護時也很麻煩。

例如,在不用defer的時候,程式碼可能會這樣寫:

func foo(i int) int {
	if i > 100 {
		fmt.Println("不是期待的數字")
		return 0
	}

	if i < 50 {
		fmt.Println("不是期待的數字")
		return 0
	}

	return i
}
複製程式碼

使用defer後,程式碼可以這樣寫

func foo(i int) int {
	defer func() {
		fmt.Println("不是期待的數字")
	}()

	if i > 100 {
		return 0
	}

	if i < 50 {
		return 0
	}

	return i
}
複製程式碼

一個函式中多個defer的執行順序是什麼?

defer在同一個函式中可以使用多次。

多個defer指定的函式執行順序是"先進後出"。

為什麼呢 ?

可以這樣理解:defer關鍵字會使其以下的程式碼先執行後再執行它指定的函式,包括其下的defer語句也會比其先執行,依此類推。

這個順序非常必要,因為在函式中,後面定義的物件可能依賴前面的物件,否則如果先出現的defer執行了,很可能造成後面的defer執行的時候出現異常。

所以,Go語言設計defer的時候是按先進後出的順序執行的

例子:

func foo() {
	i := 0
	defer func() {
		i--
		fmt.Println("第一個defer", i)
	}()

	i++
	fmt.Println("+1後的i:", i)

	defer func() {
		i--
		fmt.Println("第二個defer", i)
	}()

	i++
	fmt.Println("再+1後的i:", i)

	defer func() {
		i--
		fmt.Println("第三個defer", i)
	}()

	i++
	fmt.Println("再+1後的i:", i)
}
複製程式碼

執行後可以看到

+1後的i: 1
再+1後的i: 2
再+1後的i: 3
第三個defer 2
第二個defer 1
第一個defer 0
複製程式碼

這個過程可以看出函式執行後,先進後出執行defer並逐步處理變數的過程。

當傳遞引數給defer指定的函式時,函式延遲執行,那麼引數值會是多少?

網上有一些總結是說:defer指定的函式的引數在 defer 時確定,但,這只是一個總結,真正的原因是, Go語言除了map、slice、chan都是值傳遞

改造一下上面這個例子

func foo() {
	i := 0
	defer func(k int) {
		fmt.Println("第一個defer", k)
	}(i)

	i++
	fmt.Println("+1後的i:", i)

	defer func(k int) {
		fmt.Println("第二個defer", k)
	}(i)

	i++
	fmt.Println("再+1後的i:", i)

	defer func(k int) {
		fmt.Println("第三個defer", k)
	}(i)

	i++
	fmt.Println("再+1後的i:", i)
}
複製程式碼

得到的結果

+1後的i: 1
再+1後的i: 2
再+1後的i: 3
第三個defer 2
第二個defer 1
第一個defer 0
複製程式碼

可能會有人覺得有一點出乎預料,i在return時不是已經被計算到3了嗎?,為什麼延遲執行的defer指定的函式裡的i不是3呢?

defer關鍵字指定的函式是在return後執行的,這很容易讓人想象在return後呼叫函式。

但是,defer指定的函式是在當前行就呼叫了的,只是延遲return後執行,而不等同於“移動”到return後執行,因此呼叫時傳遞的是當前的引數的值。

傳遞指標引數會是什麼情況?

那麼如果希望defer指定的的函式引數的值是經過後面的程式碼處理過的,可以傳遞指標引數給defer指定的函式。

改造一下程式碼:

func foo() {
	i := 0
	defer func(k *int) {
		fmt.Println("第一個defer", *k)
	}(&i)

	i++
	fmt.Println("+1後的i:", i)

	defer func(k *int) {
		fmt.Println("第二個defer", *k)
	}(&i)

	i++
	fmt.Println("再+1後的i:", i)

	defer func(k *int) {
		fmt.Println("第三個defer", *k)
	}(&i)

	i++
	fmt.Println("再+1後的i:", i)
}
複製程式碼

執行後得到

+1後的i: 1
再+1後的i: 2
再+1後的i: 3
第三個defer 3
第二個defer 3
第一個defer 3
複製程式碼

defer會影響返回值嗎?

在開頭的第一個例子中可以看到,defer是在foo執行完,main裡列印返回值之前執行的,但是沒有影響到main裡的列印結果。

這還是因為相同的原則 Go語言除了map、slice、chan都是值傳遞

比較一下foo1foo2兩個函式的結果:

func main() {

	fmt.Println("foo1 return :", foo1())
	fmt.Println("foot return :", foo2())

}

func foo1() int {

	i := 0

	defer func() {
		i = 1
	}()

	return i
}

func foo2() map[string]string {

	m := map[string]string{}

	defer func() {
		m["a"] = "b"
	}()

	return m
}
複製程式碼

執行後,列印出

foo1 return : 0
foot return : map[a:b]
複製程式碼

兩個函式不同之處在於的返回值的型別,foo1中,int型別return後,defer不會影響返回結果,但是在foo2中map型別是引用傳遞,所以defer會改變返回結果。

這說明,在return時,除了map、slice、chan,其他型別return時是將值拷貝到一個臨時變數空間,因此,defer指定的函式內對函式內的變數的操作不會影響返回結果的。

還有一種情況,給函式返回值申明變數名,,這時,變數空間是在函式執行前申明出來,return時只是返回這個變數空間的內容,因此defer能夠改變返回值。

例如,改造一下foo1函式,給它的返回值申明一個變數名i

func foo1() (i int) {

	i = 0

	defer func() {
		i = 1
	}()

	return i
}
複製程式碼

再執行,可以看到 :

foo1 return : 1
複製程式碼

返回值被defer指定的函式修改了。

defer在panic和recover處理上的使用

在Go語言裡,defer有一個經典的使用場景就是recover.

在函式執行過程中,有可能在很多地方都會出現panicpanic後如果不呼叫recover,程式會退出,為了不讓程式退出,我們需要在panic後呼叫recover,但,panic後的程式碼不會執行,recover是不可能在panic後呼叫,然而panic所在的函式內defer指定的函式可以執行,所以recover只能在defer指定的函式中被呼叫,並且只需要在1個defer指定的函式中處理。

例如:

func panicfunc() {
	defer func() {
		fmt.Println("before recover")
		recover()
		fmt.Println("after recover")
	}()

	fmt.Println("before panic")
	panic(0)
	fmt.Println("after panic")
}
複製程式碼

執行後,列印出:

before panic
before recover
after recover
複製程式碼

總結以下

  1. defer語句非常重要,非常常用,必須掌握
  2. 在統一處理多個returnpanic/recover場景下使用defer
  3. 謹記“Go語言的函式引數傳遞的都是值(除了map、slice、chan)”這一重要原則,正確的評估defer指定函式的引數值
  4. defer不影響返回值,除非是map、slice和chan,或者返回值定義了變數名
  5. 執行順序:先進後出

相關文章