GO基礎筆記(一) defer詳解

defer詳解

defer是Golang一大語言特色,它扮演的是類似Java中finally的角色。用于執(zhí)行釋放資源的一些操作。

defer語句定義在函數(shù)內,后面跟一個函數(shù),被defer的函數(shù)會在defer所在的函數(shù)結果前被執(zhí)行。defer能夠保證函數(shù)不管以何種方式結束(return或panic),被defer的函數(shù)一定被執(zhí)行。

釋放資源

defer可以被定義在函數(shù)中的任何地方,這意味著當我們打開一個資源,馬上可以使用defer聲明它的釋放,這樣我們在編寫后面的代碼的時候就不用操心這個資源的釋放了。函數(shù)結束的時候會自動執(zhí)行釋放操作:

func ReadFile() error {
    // 打開了一些資源
    file, err := os.Open("file")
    if err != nil {
        return err
    }
    // 這條語句會自動在函數(shù)結束時執(zhí)行
    defer file.Close()

    // 繼續(xù)執(zhí)行后面的操作,而不用操心file的釋放了
    ...
}

這就是defer的一般用法了,當然,defer遠沒有這么簡單,下面我們來扣扣defer的語法細節(jié):

多個defer的執(zhí)行順序

如果一個函數(shù)里面有多個defer,那么會如何執(zhí)行呢?觀察下面的代碼:

package main

import "fmt"

func main() {
    deferFunc()
}

func deferFunc() {

    defer fmt.Println("exec 1")
    defer fmt.Println("exec 2")
    defer fmt.Println("exec 3")

}

注意,defer的執(zhí)行順序遵循后進先出的原則,后面的defer語句將會被先執(zhí)行,因此執(zhí)行的順序應該是從下至上的,下面是輸出:

exec 3
exec 2
exec 1

我們只需要知道defer是從下到上執(zhí)行的即可。

defer和return的順序

defer的順序還有更加深層次的話題,那就是defer和return之間的執(zhí)行順序。

觀察下面的代碼:

package main

import "fmt"

func main() {

    fmt.Println(deferReturn())
}

func deferReturn() (i int) {

    defer func() {
        i += 3
    }()

    i = 3

    return
}

執(zhí)行一下,結果輸出"6"。這說明,defer是在函數(shù)return之前執(zhí)行的。而具名返回參數(shù)i的作用域是整個函數(shù),因此理所當然地,defer可以訪問i并對其做修改。

defer和panic

當函數(shù)遇到panic,defer仍然會被執(zhí)行。Go會先執(zhí)行所有的defer鏈表(該函數(shù)的所有defer),當所有defer被執(zhí)行完畢且沒有recover時,才會進行panic。

package main

import "fmt"

func main() {

    deferPanic()
}

func deferPanic() {

    defer fmt.Println("exec 1")
    defer fmt.Println("exec 2")
    defer fmt.Println("exec 3")

    panic("出了點小小的錯誤")
}

所有的defer都沒有recover,因此會先從下到上執(zhí)行所有的defer,最后進行panic,程序退出:

exec 3
exec 2
exec 1
panic: 出了點小小的錯誤

defer+recover恢復panic

我們可以在defer中進行recover,如果defer中包含recover,則程序將不會再進行panic,觀察下面代碼:

package main

import "fmt"

func main() {

    deferPanic()
}

func deferPanic()  {

    defer fmt.Println("最后執(zhí)行")
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("發(fā)生了錯誤!")
        }
    }()
    
    fmt.Println("錯誤之前")
    panic("出了點小小的錯誤")
    fmt.Println("錯誤之后")
}

程序輸出:

錯誤之前
發(fā)生了錯誤!
最后執(zhí)行

和之前的區(qū)別就是程序沒有再panic退出了。

熟悉其它語言諸如C++、JavaPython的應該知道,這實際上就是這些語言的try...catch功能。所以所謂的golang沒有try...catch功能,處理異常都是返回error的說法其實不準確,Go是有類似try...catch的功能的,只不過寫法和其它語言不一樣。上面的代碼到Java中就是這么寫了:

public class TestTryCatch {
    public static void main(String[] args) {
        try {
            System.out.Println("錯誤之前"); 
            throw new Exception();
            System.out.Println("錯誤之后");
        } catch (Exception e) {
            System.out.Println("發(fā)生了錯誤!");
        } finally {
            System.out.Println("最后執(zhí)行");
        }
    }
}

程序輸出和上面的是一樣的。

如果是其它函數(shù)的panic,recover可以成功嗎?觀察下面代碼:

package main

import "fmt"

func main() {

    defer func() {
        if err := recover(); err != nil {
            fmt.Println("產生了錯誤!")
        }
    }()

    panicFun()

}

func panicFun() {
    panic("我是錯誤")
}

輸出:

產生了錯誤!

可見,main對panicFunc的panic進行了恢復。

而如果panicFunc自己對panic進行了恢復,則main的恢復將不會再執(zhí)行。觀察下面的代碼:

package main

import "fmt"

func main() {

    defer func() {
        if err := recover(); err != nil {
            fmt.Println("main修復錯誤!")
        }
    }()

    panicFunc()

}

func panicFunc() {
    
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("panicFunc修復錯誤!")
        }
    }()
    panic("我是錯誤")
}

程序輸出:

panicFunc修復錯誤!

main并沒有去修復panic,因為該panic已經被先執(zhí)行的panicFunc給修復掉了。

最后一個問題來,recover可以檢測到其它goroutine的panic嗎?看下面的代碼:

package main

import (
    "fmt"
    "time"
)

func main() {
    defer fmt.Println("main defer")
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("發(fā)生了錯誤!")
        }
    }()

    go func() {

        defer fmt.Println("other defer")
        panic("一些小問題")

    }()

    time.Sleep(1*time.Second)
    fmt.Println("程序執(zhí)行完畢")
}

程序輸出:

other defer
panic: 一些小問題

注意這里面有3個defer,有兩個是main的defer,其中一個進行了recover;另外一個是另一個goroutine的defer。我們觀察,盡管代碼中的goroutine是由main創(chuàng)建的,但是當其發(fā)生了panic之后,main goroutine的defer并不會被主動執(zhí)行。

但是,上面的例子退出是因為main中有recover的defer根本沒有機會執(zhí)行,那么,如果給它執(zhí)行的機會呢?panic是否會被recover?觀察下面的代碼:

package main

import (
    "fmt"
    "time"
)

func main() {
    defer fmt.Println("main defer")
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("發(fā)生了錯誤!")
        }
    }()

    go func() {

        defer fmt.Println("other defer")

        time.Sleep(2 * time.Second) // 隔久一點再產生panic
        panic("一些小問題")

    }()

    time.Sleep(1*time.Second)
    fmt.Println("程序執(zhí)行完畢")
    
    time.Sleep(4*time.Second)
}

程序輸出:

程序執(zhí)行完畢
other defer
panic: 一些小問題

還是發(fā)生了panic??梢妋ain的defer不進行recover并不是時間的原因。而是main的recover()并不能發(fā)現(xiàn)其它goroutine的panic。

我們可以總結一下:

  • defer+recover可以將當前函數(shù)中發(fā)生的panic給恢復
  • defer+recover也可以恢復當前函數(shù)的調用的沒有被recover的其它函數(shù)的panic
  • defer+recover不可以恢復其它goroutine的panic

defer參數(shù)陷阱

defer有一個非常費解的陷阱,話不多說,先看下面的代碼:

package main

import "fmt"

func main() {

    defer addAndPrint(addAndPrint(1, 2), 3)  // defer1
    defer addAndPrint(addAndPrint(4, 5), 6)  // defer2
}

func addAndPrint(a, b int) int {
    sum := a + b
    fmt.Printf("%d + %d = %d\n", a, b, sum)
    return sum
}

很多golang的新手看到這段代碼,會想,按照defer的執(zhí)行順序,應該會先執(zhí)行defer2,再執(zhí)行defer1,因此理所當然地這段代碼應該輸出:

4 + 5 = 9
9 + 6 = 15
1 + 2 = 3
3 + 3 = 6

真實運行結果讓人大跌眼鏡:

1 + 2 = 3
4 + 5 = 9
9 + 6 = 15
3 + 3 = 6

產生這個結果的原因是,在定義defer的時候,go就需要確定defer語句的函數(shù)的參數(shù)。因此go順序執(zhí)行到defer定義的時候,會直接把defer函數(shù)的參數(shù)計算出來。

在順序執(zhí)行到defer1定義時,其第一個參數(shù)是一個函數(shù),因此Go會先執(zhí)行這個函數(shù),所以第一個執(zhí)行的應該是1 + 2 = 3,隨后遇到defer2,參數(shù)也有函數(shù),執(zhí)行該函數(shù),第二個執(zhí)行的就是4 + 5 = 9。隨后函數(shù)結束,再按照defer的逆序執(zhí)行defer2,defer1。這時候它們的參數(shù)已經被確定,不再需要執(zhí)行其他函數(shù)。

也就是說,Go保證在函數(shù)執(zhí)行結束后,該defer處僅有這個函數(shù)執(zhí)行,其參數(shù)在函數(shù)結束前已經全部確定好,不需要再執(zhí)行其他函數(shù)來確定參數(shù)。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容