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

Aceld發表於2020-03-23

在用Golang開發的時候,defer這個語法也是必備的知識,但是我們除了知道他是在一個函式退出之前執行,對於defer是否還有其他地方需要注意的呢。

本文整理的defer的全場景使用情況,部分場景源自網路,加上自己的額外觀點和分析,完成了這份defer的7個隱性必備知識點。

提綱如下:

  • 知識點1: defer的執行順序
  • 知識點2:defer與return誰先誰後
  • 知識點3:函式的返回值初始化與defer間接影響
  • 知識點4:有名函式返回值遇見defer情況
  • 知識點5:defer遇見panic
  • 知識點6:defer中包含panic
  • 知識點7:defer下的函式引數包含子函式

知識點1: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

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

示例程式碼

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後的語句後執行


知識點3:函式的返回值初始化

該知識點不屬於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,而且在函式體作用域可見


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

在沒有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

知識點5: defer遇見panic

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

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

那麼,遇到panic時,遍歷本協程的defer連結串列,並執行defer。在執行defer過程中:遇到recover則停止panic,返回recover處繼續往下執行。如果沒有遇到recover,遍歷完本協程的defer連結串列後,向stderr丟擲panic資訊。

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

test10.go

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可以保證你的一些資源一定會被關閉,從而避免一些異常出現的問題。


知識點6: defer中包含panic

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

test16.go

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捕獲到。


知識點7: 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壓棧function1,壓棧函式地址、形參1、形參2(呼叫function3) –> 列印3
  • defer壓棧function2,壓棧函式地址、形參1、形參2(呼叫function4) –> 列印4
  • defer出棧function2, 呼叫function2 –> 列印2
  • defer出棧function1, 呼叫function1–> 列印1
3
4
2
1

練習:defer面試真題

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

下面程式碼輸出什麼?

test11.go

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
}
  1. 將返回值t賦值為傳入的i,此時t為1
  2. 執行return語句將t賦值給t(等於啥也沒做)
  3. 執行defer方法,將t + 3 = 4
  4. 函式返回 4
    因為t的作用域為整個函式所以修改有效。
DeferFunc2
func DeferFunc2(i int) int {
    t := i
    defer func() {
        t += 3
    }()
    return t
}
  1. 建立變數t並賦值為1
  2. 執行return語句,注意這裡是將t賦值給返回值,此時返回值為1(這個返回值並不是t)
  3. 執行defer方法,將t + 3 = 4
  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
}
  1. 首先執行return將返回值t賦值為2
  2. 執行defer方法將t + 1
  3. 最後返回 3
DeferFunc4
func DeferFunc4() (t int) {
    defer func(i int) {
        fmt.Println(i)
        fmt.Println(t)
    }(t)
    t = 1
    return 2
}
  1. 初始化返回值t為零值 0
  2. 首先執行defer的第一步,賦值defer中的func入參t為0
  3. 執行defer的第二步,將defer壓棧
  4. 將t賦值為1
  5. 執行return語句,將返回值t賦值為2
  6. 執行defer的第三步,出棧並執行
    因為在入棧時defer執行的func的入參已經賦值了,此時它作為的是一個形式引數,所以列印為0;相對應的因為最後已經將t的值修改為2,所以再列印一個2
結果
4
1
3
0
2


###關於作者:

mail: danbing.at@gmail.com
github: https://github.com/aceld
原創書籍gitbook: http://legacy.gitbook.com/@aceld

創作不易, 共同學習進步, 歡迎關注作者, 回覆”zinx”有好禮

作者微信公眾號


文章推薦

開源軟體作品

(原創開源)Zinx-基於Golang輕量級伺服器併發框架-完整版(附教程視訊)

(原創開源)Lars-基於C++負載均衡遠端排程系統-完整版

精選文章

典藏版-Golang排程器GMP原理與排程全分析

Golang三色標記、混合寫屏障GC模式圖文全分析

最常用的除錯 golang 的 bug 以及效能問題的實踐方法?

Golang中的區域性變數“何時棧?何時堆?”

使用Golang的interface介面設計原則

流?I/O操作?阻塞?epoll?

深入淺出Golang的協程池設計

Go語言構建微服務一站式解決方案


本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章