關於Go defer的詳細使用


先拋磚引玉defer的延遲調用:
defer特性:

1. 關鍵字 defer 用於注冊延遲調用。
2. 這些調用直到 return 前才被執。因此,可以用來做資源清理。
3. 多個defer語句,按先進后出的方式執行。
4. defer語句中的變量,在defer聲明時就決定了。

defer用途:

1. 關閉文件句柄
2. 鎖資源釋放
3. 數據庫連接釋放

好,廢話不多說,實例加深理解,我們先看看一段代碼

package main

import "fmt"

func main() {
    var users [5]struct{}
    for i := range users {
        defer fmt.Println(i)
    }
}

輸出:4 3 2 1 0 ,defer 是先進后出,這個輸出沒啥好說的。

我們把上面的代碼改下:
defer 換上閉包

package main

import "fmt"

func main() {
    var users [5]struct{}
    for i := range users {
        defer func() { fmt.Println(i) }()
    }
}

輸出:4 4 4 4 4,很多人也包括我。預期的結果不是 4 3 2 1 0 嗎?官網對defer 閉包的使用大致是這個意思:

函數正常執行,由於閉包用到的變量 i 在執行的時候已經變成4,所以輸出全都是4。那么 如何正常輸出預期的 4 3 2 1 0 呢?
不用閉包,換成函數:

package main

import "fmt"

func main() {
    var users [5]struct{}
    for i := range users {
        defer Print(i)
    }
}
func Print(i int) {
    fmt.Println(i)
}

函數正常延遲輸出:4 3 2 1 0。

我們再舉一個可能一不小心會犯錯的例子:
defer調用引用結構體函數

package main

import "fmt"

type Users struct {
    name string
}

func (t *Users) GetName() { // 注意這里是 * 傳地址 引用Users
    fmt.Println(t.name)
}
func main() {
    list := []Users{{"喬峰"}, {"慕容復"}, {"清風揚"}}
    for _, t := range list {
        defer t.GetName()
    }
}

輸出:清風揚 清風揚 清風揚。

這個輸出並不會像我們預計的輸出:清風揚 慕容復 喬峰

可是按照前面的go defer函數中的使用說明,應該輸出清風揚 慕容復 喬峰才對啊?

那我們換一種方式來調用一下

package main

import "fmt"

type Users struct {
    name string
}

func (t *Users) GetName() { // 注意這里是 * 傳地址 引用Users
    fmt.Println(t.name)
}
func GetName(t Users) { // 定義一個函數,名稱自定義
    t.GetName() // 調用結構體USers的方法GetName
}
func main() {
    list := []Users{{"喬峰"}, {"慕容復"}, {"清風揚"}}
    for _, t := range list {
        defer GetName(t)
    }
}

輸出:清風揚 慕容復 喬峰。

這個時候輸出的就是所謂"預期"滴了

當然,如果你不想多寫一個函數,也很簡單,可以像下面這樣(改2處),同樣會輸出清風揚 慕容復 喬峰

package main

import "fmt"

type Users struct {
    name string
}

func (t *Users) GetName() { // 注意這里是 * 傳地址 引用Users
    fmt.Println(t.name)
}
func GetName(t Users) { // 定義一個函數,名稱自定義
    t.GetName() // 調用結構體USers的方法GetName
}
func main() {
    list := []Users{{"喬峰"}, {"慕容復"}, {"清風揚"}}
    for _, t := range list {
        t2 := t // 定義新變量t2 t賦值給t2
        defer t2.GetName()
    }
}

輸出:清風揚 慕容復 喬峰。

通過以上例子

我們可以得出下面的結論:

defer后面的語句在執行的時候,函數調用的參數會被保存起來,但是不執行。也就是復制了一份。但是並沒有說struct這里的*指針如何處理,

通過這個例子可以看出go語言並沒有把這個明確寫出來的this指針(比如這里的* Users)當作參數來看待。到這里有滴朋友會說。看似多此一舉的聲明,

直接去掉指針調用 t *Users改成 t Users 不就行了?

package main

import "fmt"

type Users struct {
    name string
}

func (t Users) GetName() { // 注意這里是 * 傳地址 引用Users
    fmt.Println(t.name)
}

func main() {
    list := []Users{{"喬峰"}, {"慕容復"}, {"清風揚"}}
    for _, t := range list {
        defer t.GetName()
    }
}

輸出:清風揚 慕容復 喬峰。這就回歸到上面的 defer 函數非引用調用的示例了。所以這里我們要注意defer后面的指針函數和普通函數的調用區別。很容易混淆出錯。

多個 defer 注冊,按 FILO 次序執行 ( 先進后出 )。哪怕函數或某個延遲調用發生錯誤,這些調用依舊會被執行,我們看看這一段

package main

func users(i int) {
    defer println("北丐")
    defer println("南帝")

    defer func() {
        println("西毒")
        println(10 / i) // 異常未被捕獲,逐步往外傳遞,最終終止進程。
    }()

    defer println("東邪")
}

func main() {
    users(0)
    println("武林排行榜,這里不會被輸出哦")
}

輸出:

東邪
西毒
南帝
北丐
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.users.func1(0x0)

我們發現函數中異常,最后才捕獲輸出,但是一旦捕獲了異常,后面就不會再執行了,即終止了程序。

*延遲調用參數在求值或復制,指針或閉包會 "延遲" 讀取。

package main

func test() {
    x, y := "喬峰", "慕容復"

    defer func(s string) {
        println("defer:", s, y) // y 閉包引用 輸出延遲和的值,即y+= 后的值=慕容復第二
    }(x) // 匿名函數調用,傳送參數x 被復制,注意這里的x 是 喬峰,而不是下面的 x+= 后的值

    x += "第一"
    y += "第二"
    println("x =", x, "y =", y)
}

func main() {
    test()
}

輸出:

x = 喬峰第一 
y = 慕容復第二
defer: 喬峰 慕容復第二

defer 與 return注意

package main

import "fmt"

func Users() (s string) {

    s = "喬峰"
    defer func() {
        fmt.Println("延遲執行后:"+s)
    }()

    return "清風揚"
}

func main() {
    Users() // 輸出:延遲執行后:清風揚
}

解釋:在有命名返回值的函數中(這里命名返回值為 s),執行 return "風清揚" 的時候實際上已經將s 的值重新賦值為 風清揚。

所以defer 匿名函數 輸出結果為 風清揚 而不是 喬峰。

在錯誤的位置使用 defer,來一段不嚴謹滴代碼:

package main

import "net/http"

func request() error {
    res, err := http.Get("http://www.google.com") // 不翻牆的情況下。是無法訪問滴
    defer res.Body.Close()
    if err != nil {
        return err
    }

    // ..繼續業務code...

    return nil
}

func main() {
    request()
}

輸出:

panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x40 pc=0x5e553e]

Why?因為在這里我們並沒有檢查我們的請求是否成功執行,當它失敗的時候,我們訪問了 Body 中的空變量 res ,所以會拋出異常。

怎么優化呢?

我們應該總是在一次成功的資源分配下面使用 defer ,簡單點說就是:當且僅當 http.Get 成功執行時才使用 defer.

package main

import "net/http"

func request() error {
    res, err := http.Get("http://www.google.com")
    if res != nil {
        defer res.Body.Close()
    }

    if err != nil {
        return err
    }

    // ..繼續業務code...

    return nil
}

func main() {
    request()
}

這樣,當有錯誤的時候,err 會被返回,否則當整個函數返回的時候,會關閉 res.Body 。

解釋:在這里,同樣需要檢查 res 的值是否為 nil ,這是 http.Get 中的一個警告。

通常情況下,出錯的時候,返回的內容應為空並且錯誤會被返回,可當你獲得的是一個重定向 error 時, res 的值並不會為 nil ,

但其又會將錯誤返回。所以上面的代碼保證了無論如何 Body 都會被關閉。

另外我們再聊下關於文件的defer close。在這里,f.Close() 可能會返回一個錯誤,可這個錯誤會被我們忽略掉

我們看一段代碼:

package main

import "os"

func open() error {
    f, err := os.Open("result.json") // 確保文件名存在
    if err != nil {
        return err
    }

    if f != nil {
        defer f.Close()
    }

    // ..code...

    return nil
}

func main() {
    open()
}

表面上看似沒問題,其實f.Close可能關閉文件失敗,我們優化下:

package main

import "os"

func open() error {
    f, err := os.Open("result.json")
    if err != nil {
        return err
    }

    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                return
            }
        }()
    }

    // ..code...

    return nil
}

func main() {
    open()
}

如果有代碼潔癖優化強迫症滴,哈哈。這里我們還可以優化下,可以通過命名的返回變量來返回 defer 內的錯誤。 如下:

package main

import "os"

func open() (err error) {
    f, err := os.Open("result.json")
    if err != nil {
        return err
    }

    if f != nil {
        defer func() {
            if ferr := f.Close(); ferr != nil {
                err = ferr //這里 通過命名的返回變量ferr賦值給err 來返回 defer 內的錯誤
            }
        }()
    }

    // ..code...

    return nil
}

func main() {
    open()
}

最后一個容易忽視的問題:如果你嘗試使用相同的變量釋放不同的資源,那么這個操作可能無法正常執行

神馬意思?繼續看:

package main

import (
    "fmt"
    "os"
)

func open() error {
    f, err := os.Open("result.json")
    if err != nil {
        return err
    }
    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                fmt.Printf("延遲關閉文件result.json 錯誤 %v\n", err)
            }
        }()
    }

    // ..code...

    f, err = os.Open("result2.json")
    if err != nil {
        return err
    }
    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                fmt.Printf("延遲關閉文件result2.json 錯誤 %v\n", err)
            }
        }()
    }

    return nil
}

func main() {
    open()
}

輸出:

延遲關閉文件result.json 錯誤 close result2.json: file already closed

結論:當延遲函數執行時,只有最后一個變量會被用到,因此,f 變量 會成為最后那個資源 (result2.json)。

而且兩個 defer 都會將這個資源作為最后的資源來關閉,也就是優先關閉了result2.json后,再執行第一個defer Close result1.json的時候,

其實還是在關閉result2.json.這樣重復關閉同一個文件導致錯誤異常。腫么解決?很好辦?用io.Closer屬性

package main

import (
    "fmt"
    "io"
    "os"
)

func open() error {
    f, err := os.Open("result.json")
    if err != nil {
        return err
    }
    if f != nil {
        defer func(f io.Closer) { // 注意修改滴地方
            if err := f.Close(); err != nil {
                fmt.Printf("延遲關閉文件result.json 錯誤 %v\n", err)
            }
        }(f) // 注意修改滴地方
    }

    // ..code...

    f, err = os.Open("result2.json")
    if err != nil {
        return err
    }
    if f != nil {
        defer func(f io.Closer) {// 注意修改滴地方
            if err := f.Close(); err != nil {
                fmt.Printf("延遲關閉文件result2.json 錯誤 %v\n", err)
            }
        }(f)// 注意修改滴地方
    }

    return nil
}

func main() {
    open()
}

到此,關於Go中defer的使用總結到這里了,有更多的使用技巧或坑,歡迎諸位博友留言指正。。。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM