Go基礎系列:defer、panic和recover

駿馬金龍發表於2018-10-30

defer關鍵字

defer關鍵字可以讓函式或語句延遲到函式語句塊的最結尾時,即即將退出函式時執行,即便函式中途報錯結束、即便已經panic()、即便函式已經return了,也都會執行defer所推遲的物件。

例如:

func main() {
    a()
}

func a() {
    println("in a")
    defer b()
    println("leaving a")
    //到了這裡才會執行b()
}

func b() {
    println("in b")
    println("leaving b")
}

上面將輸出:

in a
leaving a
in b
leaving b

即便是函式已經報錯,或函式已經return返回,defer的物件也會在函式退出前的最後一刻執行。

func a() TYPE{
    ...CODE...
    
    defer b()
    
    ...CODE...
    
    // 函式執行出了錯誤
    
    return args
    // 函式b()都會在這裡執行
}

但注意,由於Go的作用域採用的是詞法作用域,defer的定義位置決定了它推遲物件能看見的變數值,而不是推遲物件被呼叫時所能看見的值。

例如:

package main

var x = 10
func main() {
    a()
}

func a() {
    println("start a:",x)   // 輸出10
    x = 20
    defer b(x)
    x = 30
    println("leaving a:",x)  // 輸出30
    // 呼叫defer延遲的物件b(),輸出20
}

func b(x int) {
    println("start b:",x)
}

比較下面的defer:

package main

var x = 10

func main() {
    a()
}

func a() int {
    println("start a:", x) // 輸出10
    x = 20
    defer func() {
        println("in defer:", x)  // 輸出30
    }()
    x = 30
    println("leaving a:", x) // 輸出30
    return x
}

上面defer推遲的匿名函式輸出的值是30,它看見的不應該是20嗎?先再改成下面的:

package main

var x = 10

func main() {
    a()
}

func a() int {
    println("start a:", x) // 輸出10
    x = 20
    defer func(x int) {
        println("in defer:", x)  // 輸出20
    }(x)
    x = 30
    println("leaving a:", x) // 輸出30
    return x
}

這個defer推遲的物件中看見的卻是20,這和第一種defer b(x)是相同的。

原因在於defer推遲的如果是函式,它直接就在它的定義位置處評估好引數、變數。該拷貝傳值的的拷貝傳值,該指標相見的指標相見。所以,對於第(1)和第(3)種情況,在defer的定義位置處,就將x=20拷貝給了推遲的函式引數,所以函式內部操作的一直是x的副本。而第二種情況則是直接指向它所看見的x=20那個變數,則個變數是全域性變數,當執行x=30的時候會將其值修改,到執行defer推遲的物件時,它指向的x的值已經是修改過的。

再看下面這個例子,將defer放進一個語句塊中,並在這個語句塊中新宣告一個同名變數x:

func a() int {
    println("start a:", x) // 輸出10
    x = 20
    {
        x := 40
        defer func() {
            println("in defer:", x)  // 輸出40
        }()
    }
    x = 30
    println("leaving a:", x) // 輸出30
    return x
}

上面的defer定義在語句塊中,它能看見的x是語句塊中x=40,它的x指向的是語句塊中的x。另一方面,當語句塊結束時,x=40的x會消失,但由於defer的函式中仍有x指向40這個值,所以40這個值仍被defer的函式引用著,它直到defer執行完之後才會被GC回收。所以defer的函式在執行的時候,仍然會輸出40。

如果語句塊內有多個defer,則defer的物件以LIFO(last in first out)的方式執行,也就是說,先定義的defer後執行。

func main() {
    println("start...")
    defer println("1")
    defer println("2")
    defer println("3")
    defer println("4")
    println("end...")
}

將輸出:

start...
end...
4
3
2
1

defer有什麼用呢?一般用來做善後操作,例如清理垃圾、釋放資源,無論是否報錯都執行defer物件。另一方面,defer可以讓這些善後操作的語句和開始語句放在一起,無論在可讀性上還是安全性上都很有改善,畢竟寫完開始語句就可以直接寫defer語句,永遠也不會忘記關閉、善後等操作。

例如,開啟檔案,關閉檔案的操作寫在一起:

open()
defer file.Close()
... 操作檔案 ...

以下是defer的一些常用場景:

  • 開啟關閉檔案
  • 鎖定、釋放鎖
  • 建立連線、釋放連線
  • 作為結尾輸出結尾資訊
  • 清理垃圾(如臨時檔案)

panic()和recover()

panic()用於產生錯誤資訊並終止當前的goroutine,一般將其看作是退出panic()所在函式以及退出呼叫panic()所在函式的函式。例如,G()中呼叫F(),F()中呼叫panic(),則F()退出,G()也退出。

注意,defer關鍵字推遲的物件是函式最後呼叫的,即使出現了panic也會呼叫defer推遲的物件。

例如,下面的程式碼中,main()中輸出一個start main之後呼叫a(),它會輸出start a,然後就panic了,panic()會輸出panic: panic in a,然後報錯,終止程式。

func main() {
    println("start main")
    a()
    println("end main")
}

func a() {
    println("start a")
    panic("panic in a")
    println("end a")
}

執行結果如下:

start main
start a
panic: panic in a

goroutine 1 [running]:
main.a()
        E:/learning/err.go:14 +0x63
main.main()
        E:/learning/err.go:8 +0x4c
exit status 2

注意上面的end aend main都沒有被輸出。

可以使用recover()去捕獲panic()並恢復執行。recover()用於捕捉panic()錯誤,並返回這個錯誤資訊。但注意,即使recover()捕獲到了panic(),但呼叫含有panic()函式的函式(即上面的G()函式)也會退出,所以如果recover()定義在G()中,則G()中呼叫F()函式之後的程式碼都不會執行(見下面的通用格式)。

以下是比較通用的panic()和recover()的格式:

func main() {
    G()
    // 下面的程式碼會執行
    ...CODE IN MAIN...
}
func G(){
    defer func (){
        if str := recover(); str != nil {
            fmt.Println(str)
        }
    }()
    ...CODE IN G()...
    
    // F()的呼叫必須在defer關鍵字之後
    F()
    // 該函式內下面的程式碼不會執行
    ...CODE IN G()...
}
func F() {
    ...CODE1...
    panic("error found")
    // 下面的程式碼不會執行
    ...CODE IN F()...
}

可以使用recover()去捕獲panic()並恢復執行。但以下程式碼是錯誤的:

func main() {
    println("start main")
    a()
    println("end main")
}

func a() {
    println("start a")
    panic("panic in a")

    // 直接放在panic後是錯誤的
    panic_str := recover()
    println(panic_str)

    println("end a")
}

之所以錯誤,是因為panic()一出現就直接退出函式a()和main()了。要想recover()真正捕獲panic(),需要將recover()放在defer的推遲物件中,且defer的定義必須在panic()發生之前。

例如,下面是通用格式的示例:

package main

import "fmt"

func main() {
    println("start main")
    b()
    println("end main")
}

func a() {
    println("start a")
    panic("panic in a")
    println("end a")
}

func b() {
    println("start b")
    defer func() {
        if str := recover(); str != nil {
            fmt.Println(str)
        }
    }()
    a()
    println("end b")
}

以下是輸出結果:

start main
start b
start a
panic in a
end main

注意上面的end bend a都沒有被輸出,但是end main輸出了。

panic()是內建的函式(在包builtin中),在log包中也有一個Panic()函式,它呼叫Print()輸出資訊後,再呼叫panic()。go doc log Panic一看便知:

$ go doc log Panic
func Panic(v ...interface{})
    Panic is equivalent to Print() followed by a call to panic().

相關文章