Golang中的Defer必掌握的7知識點

2021-02-23 Go語言中文網
在用Golang開發的時候,defer這個語法也是必備的知識,但是我們除了知道他是在一個函數退出之前執行,對於defer是否還有其他地方需要注意的呢。本文整理的defer的全場景使用情況,部分場景源自網絡,加上自己的額外觀點和分析,完成了這份defer的7個隱性必備知識點。

知識點1:defer的執行順序

知識點2:  defer與return誰先誰後

知識點3:函數的返回值初始化與defer間接影響

知識點4:有名函數返回值遇見defer情況

知識點5:defer遇見panic

知識點6:defer中包含panic

知識點7:defer下的函數參數包含子函數

多個defer出現的時候,它是一個「棧」的關係,也就是先進後出。一個函數中,寫在前面的defer會比寫在後面的defer調用的晚。

示例代碼

package main

import "fmt"

func main() {
defer func1()
defer func2()
defer func3()
}

func func1() {
fmt.Println("A")
}

func func2() {
fmt.Println("B")
}

func func3() {
fmt.Println("C")
}

輸出結果:

C
B
A


示例代碼

package main

import "fmt"

func deferFunc() int {
fmt.Println("defer func called")
return 0
}

func returnFunc() int {
fmt.Println("return func called")
return 0
}

func returnAndDefer() int {

defer deferFunc()

return returnFunc()
}

func main() {
returnAndDefer()
}

執行結果為:

return func called
defer func called

結論為:return之後的語句先執行,defer後的語句後執行


該知識點不屬於defer本身,但是調用的場景卻與defer有聯繫,所以也算是defer必備了解的知識點之一。

如 : func DeferFunc1(i int) (t int) {}
其中返回值t int,這個t會在函數起始處被初始化為對應類型的零值並且作用域為整個函數。

示例代碼

package main

import "fmt"

func DeferFunc(i int) (t int) {

fmt.Println("t = ", t)

return 2
}

func main() {
DeferFunc(10)
}

結果

t = 0

證明,只要聲明函數的返回值變量名稱,就會在函數初始化時候為之賦值為0,而且在函數體作用域可見。

在沒有defer的情況下,其實函數的返回就是與return一致的,但是有了defer就不一樣了。

我們通過知識點2得知,先return,再defer,所以在執行完return之後,還要再執行defer裡的語句,依然可以修改本應該返回的結果。

package main

import "fmt"

func returnButDefer() (t int) { //t初始化0, 並且作用域為該函數全域

defer func() {
t = t * 10
}()

return 1
}

func main() {
fmt.Println(returnButDefer())
}

該returnButDefer()本應的返回值是1,但是在return之後,又被defer的匿名func函數執行,所以t=t*10被執行,最後returnButDefer()返回給上層main()的結果為10

$ go run test.go
10



我們知道,能夠觸發defer的是遇見return(或函數體到末尾)和遇見panic。

根據知識點2,defer遇見return情況如下:

那麼,遇到panic時,遍曆本協程的defer鍊表,並執行defer。在執行defer過程中:遇到recover則停止panic,返回recover處繼續往下執行。如果沒有遇到recover,遍歷完本協程的defer鍊表後,向stderr拋出panic信息。

A. defer遇見panic,但是並不捕獲異常的情況

package main

import (
"fmt"
)

func main() {
defer_call()

fmt.Println("main 正常結束")
}

func defer_call() {
defer func() { fmt.Println("defer: panic 之前1") }()
defer func() { fmt.Println("defer: panic 之前2") }()

panic("異常內容") //觸發defer出棧

defer func() { fmt.Println("defer: panic 之後,永遠執行不到") }()
}

結果

defer: panic 之前2
defer: panic 之前1
panic: 異常內容
//... 異常堆棧信息

B. defer遇見panic,並捕獲異常

package main

import (
"fmt"
)

func main() {
defer_call()

fmt.Println("main 正常結束")
}

func defer_call() {

defer func() {
fmt.Println("defer: panic 之前1, 捕獲異常")
if err := recover(); err != nil {
fmt.Println(err)
}
}()

defer func() { fmt.Println("defer: panic 之前2, 不捕獲") }()

panic("異常內容") //觸發defer出棧

defer func() { fmt.Println("defer: panic 之後, 永遠執行不到") }()
}

結果

defer: panic 之前2, 不捕獲
defer: panic 之前1, 捕獲異常
異常內容
main 正常結束

defer 最大的功能是 panic 後依然有效
所以defer可以保證你的一些資源一定會被關閉,從而避免一些異常出現的問題。


編譯執行下面代碼會出現什麼?

package main

import (
"fmt"
)

func main() {

defer func() {
if err := recover(); err != nil{
fmt.Println(err)
}else {
fmt.Println("fatal")
}
}()

defer func() {
panic("defer panic")
}()

panic("panic")
}

結果

defer panic

分析

panic僅有最後一個可以被revover捕獲。

觸發panic("panic")後defer順序出棧執行,第一個被執行的defer中 會有panic("defer panic")異常語句,這個異常將會覆蓋掉main中的異常panic("panic"),最後這個異常被第二個執行的defer捕獲到。



package main

import "fmt"

func function(index int, value int) int {

fmt.Println(index)

return index
}

func main() {
defer function(1, function(3, 0))
defer function(2, function(4, 0))
}

這裡,有4個函數,他們的index序號分別為1,2,3,4。

那麼這4個函數的先後執行順序是什麼呢?這裡面有兩個defer, 所以defer一共會壓棧兩次,先進棧1,後進棧2。那麼在壓棧function1的時候,需要連同函數地址、函數形參一同進棧,那麼為了得到function1的第二個參數的結果,所以就需要先執行function3將第二個參數算出,那麼function3就被第一個執行。同理壓棧function2,就需要執行function4算出function2第二個參數的值。然後函數結束,先出棧fuction2、再出棧function1.

所以順序如下:

defer出棧function2

調用function2 --> 列印2

defer出棧function1

調用function1--> 列印1

3
4
2
1


了解以上7個defer的知識點,我們來驗證一下網上的真題吧。

下面代碼輸出什麼?

package main

import "fmt"

func DeferFunc1(i int) (t int) {
t = i
defer func() {
t += 3
}()
return t
}

func DeferFunc2(i int) int {
t := i
defer func() {
t += 3
}()
return t
}

func DeferFunc3(i int) (t int) {
defer func() {
t += i
}()
return 2
}

func DeferFunc4() (t int) {
defer func(i int) {
fmt.Println(i)
fmt.Println(t)
}(t)
t = 1
return 2
}

func main() {
fmt.Println(DeferFunc1(1))
fmt.Println(DeferFunc2(1))
fmt.Println(DeferFunc3(1))
DeferFunc4()
}

練習題分析DeferFunc1

func DeferFunc1(i int) (t int) {
t = i
defer func() {
t += 3
}()
return t
}

將返回值t賦值為傳入的i,此時t為1

執行return語句將t賦值給t(等於啥也沒做)

執行defer方法,將t + 3 = 4

函數返回 4
因為t的作用域為整個函數所以修改有效。

DeferFunc2

func DeferFunc2(i int) int {
t := i
defer func() {
t += 3
}()
return t
}

創建變量t並賦值為1

執行return語句,注意這裡是將t賦值給返回值,此時返回值為1(這個返回值並不是t)

執行defer方法,將t + 3 = 4

函數返回返回值1

也可以按照如下代碼理解

func DeferFunc2(i int) (result int) {
t := i
defer func() {
t += 3
}()
return t
}

上面的代碼return的時候相當於將t賦值給了result,當defer修改了t的值之後,對result是不會造成影響的。

DeferFunc3

func DeferFunc3(i int) (t int) {
defer func() {
t += i
}()
return 2
}

首先執行return將返回值t賦值為2

執行defer方法將t + 1

最後返回 3

DeferFunc4

func DeferFunc4() (t int) {
defer func(i int) {
fmt.Println(i)
fmt.Println(t)
}(t)
t = 1
return 2
}

初始化返回值t為零值 0

首先執行defer的第一步,賦值defer中的func入參t為0

執行defer的第二步,將defer壓棧

將t賦值為1

執行return語句,將返回值t賦值為2

執行defer的第三步,出棧並執行
因為在入棧時defer執行的func的入參已經賦值了,此時它作為的是一個形式參數,所以列印為0;相對應的因為最後已經將t的值修改為2,所以再列印一個2

結果

4
1
3
0
2

推薦閱讀

喜歡本文的朋友,歡迎關注「Go語言中文網」:

Go語言中文網啟用微信學習交流群,歡迎加微信:274768166,投稿亦歡迎

相關焦點

  • 聊聊golang的defer
    序本文主要研究一下golang的deferdeferreturn先賦值(對於命名返回值),然後執行defer,最後函數返回
  • Golang 語言中的 defer 怎麼使用?
    defer 所在的函數或方法中,如果調用 os.Exit(1),defer 即便註冊,也不會執行。defer 必須在函數和方法中才可以使用,並且 defer 後面必須是函數(自定義和部分內置函數)或方法,defer 函數的實參是值拷貝。
  • golang中defer的執行過程詳解
    中defer的執行過程是怎樣的?defer是go的關鍵字。本文通過go彙編信息,深入分析了defer的調用棧原理在同一個goroutine中:多個defer的調用棧原理是什麼?defer函數是如何調用的?
  • Golang中defer的實現原理
    :定義defer類似於入棧操作,執行defer類似於出棧操作,先進後出3.defer可操作主函數返回值defer語句中的函數會在return語句更新返回值後在執行。defer實現原理注意:我會把源碼中每個方法的作用都注釋出來,可以參考注釋進行理解。
  • Golang中defer的使用三特性
    -2014-05-25.htmlGolang中的defer關鍵字實現比較特殊的功能,按照官方的解釋,defer後面的表達式會被放入一個列表中,在當前方法返回的時候,列表中的表達式就會被執行。一個方法中可以在一個或者多個地方使用defer表達式,這也是前面提到的,為什麼需要用一個列表來保存這些表達式。在Golang中,defer表達式通常用來處理一些清理和釋放資源的操作。defer的行為稍微複雜一些,想要徹底理解defer,需要了解Golang中defer相關的一些特性。
  • Golang-Defer
    Go官方文檔中對defer的執行時機做了闡述,分別是。包裹defer的函數返回時包裹defer的函數執行到末尾時所在的goroutine發生panic時defer執行順序當一個方法中有多個defer時, defer會將要延遲執行的方法「壓棧」,當defer被觸發時,將所有「壓棧」的方法「出棧」並執行。
  • Go 經典入門系列 28:Defer
    largest 函數接收一個 int 類型的切片[3]作為參數,然後列印出該切片中的最大值。largest 函數的第一行的語句為 defer finished()。這表示在 finished() 函數將要返回之前,會調用 finished() 函數。
  • Golang之defer進階
    fmt.Printf("in defer: x = %d\n", x) x = 7 return 9}func test2() (x int) { x = 7 defer fmt.Printf("in defer: x = %d\n", x) return 9}func test3() (x int) { defer func() { fmt.Printf("in
  • 《8小時轉職Golang工程師》
    置頂 本視頻偏入門級,主要是針對後端想快速低成本掌握golang開發人群學習,如您已經掌握golang請繞行。
  • 【Golang】脫胎換骨的defer
    ,main函數註冊的defer函數是A,所以,defer鍊表項中_defer.fn存儲的是A的funcval指針。2. defer信息保存到鍊表,而鍊表操作比較慢。但是,defer作為一個關鍵的語言特性,怎能如此受人詬病?所以GO語言在1.13和1.14中做出了不同的優化。defer1.13:先提個30%Go1.13中defer性能的優化點,主要集中在減少defer結構體堆分配。我們通過一個例子,看看它是怎樣做到的。
  • golang的defer使用相關
    ()) fmt.Println("-") deferFunc3() fmt.Println("-") deferFunc4()}func deferFunc1() { i := 1 defer fmt.Print(i) i = 2 return}func deferFunc2() (result int) { i := 1 defer func() { result
  • golang channel 使用總結
    本文介紹了使用 golang channel 的諸多特性和技巧,已經熟悉了 go 語言特性的小夥伴也可以看看,很有啟發。channel 中讀取數據時,內部的操作如下:當 buf 非空時,此時 recvq 必為空,buf 彈出一個元素給讀協程,讀協程獲得數據後繼續執行,此時若 sendq 非空,則從 sendq 中彈出一個寫協程轉入 running 狀態,待寫數據入隊列 buf ,此時讀取操作 <- ch 未阻塞;當 buf 為空但 sendq 非空時(不帶緩衝的 channel),則從 sendq 中彈出一個寫協程轉入
  • golang中Context的使用場景
    golang中Context的使用場景context在Go1.7之後就進入標準庫中了。它主要的用處如果用一句話來說,是在於控制goroutine的生命周期。當一個計算任務被goroutine承接了之後,由於某種原因(超時,或者強制退出)我們希望中止這個goroutine的計算任務,那麼就用得到這個Context了。
  • Golang入門教程——基本操作篇
    defergolang的函數當中有一個特殊的用法,就是defer。這個用法據說其他語言也有,但是我暫時沒有見到過。defer是一個關鍵字,用它修飾的語句會被存入棧中,直到函數退出的時候執行。所以這個時候我們通常都會用defer來執行文件的關閉。要注意的是,defer修飾的代碼會被放入棧中。所以最後會按照先進後出的原則進行執行。
  • go 學習筆記之解讀什麼是defer延遲函數
    Go 語言中有個 defer 關鍵字,常用於實現延遲函數來保證關鍵代碼的最終執行,常言道: "未雨綢繆方可有備無患".[5]Go 語言中 defer 的一些坑[6]go defer (go 延遲函數)[7]參考資料[1]Defer
  • Golang 之輕鬆化解 defer 的溫柔陷阱
    >上面的代碼中就用到了defer的原理,defer函數定義的時候,參數就已經複製進去了,之後,真正執行close()函數的時候就剛好關閉的是正確的「文件」了,妙哉!空的returnreturn}因此,main函數中調用f()得到1。defer語句的參數defer語句表達式的值在定義時就已經確定了。
  • 深入剖析 defer 原理篇 —— 函數調用的原理?
    地址空間函數棧幀棧幀的劃定函數調用函數返回舉個例子總結本篇文章是深入剖析 golang 的 defer 的基礎知識準備,如果要完全理解 defer ,避免踩坑,這個章節的基礎知識必不可少這個對理解 defer 在函數裡的行為必不可少。那麼,當你看到一個函數調用的語句你能回憶起多少知識點呢?下圖是一個典型的作業系統的地址空間示意圖:最重要的幾點:內核棧在高地址,用戶棧在低地址。
  • Go 語言之 defer 的前世今生 - CSDN
    () {5println("changkun.de/golang")6 }()7 }8}因而 defer 並不是免費的午餐,在一個複雜的調用中,當無法直接確定需要的產生的延遲調用的數量時,延遲語句將導致運行性能的下降。
  • Go 語言之 defer 的前世今生
    /golang")6    }()7  }8}因而 defer 並不是免費的午餐,在一個複雜的調用中,當無法直接確定需要的產生的延遲調用的數量時,延遲語句將導致運行性能的下降。如果一個與 defer 出現在循環語句中,則可執行的次數可能無法在編譯期決定;如果一個調用中 defer 由於數量過多等原因,不能被編譯器進行開放編碼,則也會在堆上分配 defer。總之,由於這種不確定性的存在,在堆上分配的 defer 需要最多的運行時支持,因而產生的運行時開銷也最大。
  • golang標準庫log
    log.Print("my log") log.Printf("my log %d", 100) name := "tom" age := 20 log.Println(name, ",", age) log.Panic("致命錯誤!") // log.Fatal("致命錯誤!")