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
初始化為0
,defer
指定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都是值傳遞
比較一下foo1
和foo2
兩個函式的結果:
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
.
在函式執行過程中,有可能在很多地方都會出現panic
,panic
後如果不呼叫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
複製程式碼
總結以下
- defer語句非常重要,非常常用,必須掌握
- 在統一處理多個
return
和panic/recover
場景下使用defer - 謹記“Go語言的函式引數傳遞的都是值(除了map、slice、chan)”這一重要原則,正確的評估defer指定函式的引數值
- defer不影響返回值,除非是map、slice和chan,或者返回值定義了變數名
- 執行順序:先進後出