go defer 學習筆記

ramsey發表於2021-08-29

什麼是defer?

在Go中,一個函式呼叫可以跟在一個defer關鍵字後面,形成一個延遲函式呼叫。
當一個函式呼叫被延遲後,它不會立即被執行。它將被推入由當前協程維護的一個延遲呼叫堆疊。 當一個函式呼叫(可能是也可能不是一個延遲呼叫)返回並進入它的退出階段後,所有在此函式呼叫中已經被推入的延遲呼叫將被按照它們被推入堆疊的順序逆序執行。 當所有這些延遲呼叫執行完畢後,此函式呼叫也就真正退出了。
舉個簡單的例子:

package main

import "fmt"

func sum(a, b int) {
    defer fmt.Println("sum函式即將返回")
    defer fmt.Println("sum函式finished")
    fmt.Printf("引數a=%v,引數b=%v,兩數之和為%v\n", a, b, a+b)
}

func main() {
    sum(1, 2)
}

output:

引數a=1,引數b=2,兩數之和為3
sum函式finished
sum函式即將返回

事實上,每個協程維護著兩個呼叫堆疊。

  • 一個是正常的函式呼叫堆疊。在此堆疊中,相鄰的兩個呼叫存在著呼叫關係。晚進入堆疊的呼叫被早進入堆疊的呼叫所呼叫。 此堆疊中最早被推入的呼叫是對應協程的啟動呼叫。
  • 另一個堆疊是上面提到的延遲呼叫堆疊。處於延遲呼叫堆疊中的任意兩個呼叫之間不存在呼叫關係。

defer函式引數估值

  • 對於一個延遲函式呼叫,它的實參是在此呼叫被推入延遲呼叫堆疊的時候被估值的。
  • 一個匿名函式體內的表示式是在此函式被執行的時候才會被逐個估值的,不管此函式是被普通呼叫還是延遲呼叫。
    例子1:
package main

import  "fmt"

func  Print(a int) {

fmt.Println("defer函式中a的值=", a)

}

func  main() {

a := 10

defer  Print(a)

a = 1000

fmt.Println("a的值=", a)

}

output:

a的值= 1000
defer函式中a的值= 10

defer Print(a) 被加入到延遲呼叫堆疊的時候,a 的值是5,故defer Print(a) 輸出的結果為5
例子2:

package main

import "fmt"

func main() {
    func() {
        for i := 0; i < 3; i++ {
            defer fmt.Println("a=", i)
        }
    }()

    fmt.Println()
    func() {
        for i := 0; i < 3; i++ {
            defer func() {
                fmt.Println("b=", i)
            }()
        }
    }()
}

output:

a= 2
a= 1
a= 0

b= 3
b= 3
b= 3

第一個匿名函式迴圈中的 i 是在 fmt.Println函式呼叫被推入延遲呼叫堆疊的時候估的值,因此輸出結果是 2,1,0 , 第二個匿名函式中的 i 是匿名函式呼叫退出階段估的值(此時 i 已經變成3了),故結果輸出:3,3,3。
其實對第二個匿名函式呼叫略加修改,就能使它輸出和匿名函式一相同的結果:

package main

import "fmt"

func main() {
    func() {
        for i := 0; i < 3; i++ {
            defer fmt.Println("a=", i)
        }
    }()

    fmt.Println()
    func() {
        for i := 0; i < 3; i++ {
            defer func(i int) {
                fmt.Println("b=", i)
            }(i)
        }
    }()
}

output:

a= 2
a= 1
a= 0

b= 2
b= 1
b= 0

恐慌(panic)和恢復(defer + recover)

Go不支援異常丟擲和捕獲,而是推薦使用返回值顯式返回錯誤。 不過,Go支援一套和異常丟擲/捕獲類似的機制。此機制稱為恐慌/恢復(panic/recover)機制。

我們可以呼叫內建函式panic來產生一個恐慌以使當前協程進入恐慌狀況。

進入恐慌狀況是另一種使當前函式呼叫開始返回的途徑。 一旦一個函式呼叫產生一個恐慌,此函式呼叫將立即進入它的退出階段,在此函式呼叫中被推入堆疊的延遲呼叫將按照它們被推入的順序逆序執行。

透過在一個延遲函式呼叫之中呼叫內建函式recover,當前協程中的一個恐慌可以被消除,從而使得當前協程重新進入正常狀況。

在一個處於恐慌狀況的協程退出之前,其中的恐慌不會蔓延到其它協程。 如果一個協程在恐慌狀況下退出,它將使整個程式崩潰。看下面的兩個例子:

package main

import (
    "fmt"
    "time"
)

func div(a, b int) int {
    return a / b
}

func main() {
    go func() {
        fmt.Println(div(1, 0))
    }()
    time.Sleep(time.Second)
    fmt.Println("程式正常退出~~~")
}

output:

panic: runtime error: integer divide by zero

goroutine 6 [running]:
main.div(...)
        /Users/didi/Desktop/golang/defer.go:9
main.main.func1()
        /Users/didi/Desktop/golang/defer.go:14 +0x12
created by main.main
        /Users/didi/Desktop/golang/defer.go:13 +0x39
exit status 2

div函式發生panic(除數為0),因為所在協程沒有恐慌恢復機制,導致整個程式崩潰。
如果div函式所在協程加上恐慌恢復(defer + recover),程式便可正常退出。

package main

import (
    "fmt"
    "time"
)

func div(a, b int) int {
    return a / b
}

func main() {
    go func() {
        defer func() {
            v := recover()
            if v != nil {
                fmt.Println("恐慌被恢復了:", v)
            }
        }()
        fmt.Println(div(1, 0))
    }()
    time.Sleep(time.Second)
    fmt.Println("程式正常退出~~~")
}

output:

恐慌被恢復了: runtime error: integer divide by zero
程式正常退出~~~

參考文章:gfw.go101.org/article/control-flow...

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

相關文章