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

defer:在函數A內用defer關鍵字調用的函數B會在在函數A return後執行。golang

golang defer

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

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打印返回值。函數

有什麼用途?

函數中會申明使用不少變量資源,函數結束時,咱們一般會對它們作一些處理:銷燬、釋放(例如數據庫連接、文件句柄、流)。spa

通常狀況下,咱們會在return語句以前處理這些事情。設計

可是,若是函數中包含多個return,這些處理咱們須要在每一個return以前都操做一次,實際工做中常常出現遺漏,代碼維護時也很麻煩。指針

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

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

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

    return i
}

使用defer後,代碼能夠這樣寫對象

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

    if i > 100 {
        i = 0
        return i
    }

    if i < 50 {
        i = 0
        return i
    }

    return i
}

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

defer在同一個函數中可使用屢次。blog

多個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. 執行順序:先進後出
相關文章
相關標籤/搜索