[譯] part 29: golang defer

咔嘰咔嘰發表於2019-04-01

什麼是 Defer

在存在defer語句的函式返回之前,會執行defer的呼叫。定義可能看起來有點難懂,但通過示例來理解它非常簡單。

package main

import (
    "fmt"
)

func finished() {
    fmt.Println("Finished finding largest")
}

func largest(nums []int) {
    defer finished()
    fmt.Println("Started finding largest")
    max := nums[0]
    for _, v := range nums {
        if v > max {
            max = v
        }
    }
    fmt.Println("Largest number in", nums, "is", max)
}

func main() {
    nums := []int{78, 109, 2, 563, 300}
    largest(nums)
}
複製程式碼

Run in playgroud 以上是一個簡單的程式,用於查詢給定切片最大的數。largest函式將int切片作為引數,並輸出該切片的最大數。largest函式的第一行包含語句defer finished()。這意味著在largest函式返回之前將呼叫finished函式。執行此程式,可以看到以下輸出。

Started finding largest
Largest number in [78 109 2 563 300] is 563
Finished finding largest
複製程式碼

largest函式開始執行並列印上述輸出的前兩行。在它返回之前,defer函式完成執行,並列印Finished finding largest :)

defer一個方法

defer不僅限於函式。defer呼叫方法也是完全合法的。讓我們寫一個小程式來測試它。

package main

import (
    "fmt"
)


type person struct {
    firstName string
    lastName string
}

func (p person) fullName() {
    fmt.Printf("%s %s",p.firstName,p.lastName)
}

func main() {
    p := person {
        firstName: "John",
        lastName: "Smith",
    }
    defer p.fullName()
    fmt.Printf("Welcome ")
}
複製程式碼

Run in playground

在上面的程式中,我們defer了一個方法的呼叫,其餘的程式碼是不難懂的。該程式輸出,

Welcome John Smith
複製程式碼

defer的引數作用域

defer的函式的引數是在執行defer語句時傳入的,在實際函式呼叫的時候defer函式的引數還是當初傳入的引數。

來一個例子,

package main

import (
    "fmt"
)

func printA(a int) {
    fmt.Println("value of a in deferred function", a)
}
func main() {
    a := 5
    defer printA(a)
    a = 10
    fmt.Println("value of a before deferred function call", a)

}
複製程式碼

Run in playgroud

在上面的程式中,第 11 行a被初始化為 5,defer語句實在第 12 行。adefer函式printA的引數。在第 13 行我們將a的值更改為 10。該程式的輸出,

value of a before deferred function call 10
value of a in deferred function 5
複製程式碼

從上面的輸出可以看到,儘管在執行defer語句之後a的值變為 10,但實際的defer函式呼叫printA(a)仍然列印 5。

多個defer函式的呼叫順序

當一個函式有多個defer呼叫時,它們會被新增到棧中並以後進先出(LIFO)的順序執行。 我們將編寫一個小程式,使用一系列defer來反向列印字串。

package main

import (
    "fmt"
)

func main() {
    name := "Naveen"
    fmt.Printf("Orignal String: %s\n", string(name))
    fmt.Printf("Reversed String: ")
    for _, v := range []rune(name) {
        defer fmt.Printf("%c", v)
    }
}
複製程式碼

Run in playgroud

在上面的程式中,第 11 行開始的for range迴圈迭代字串並呼叫defer fmt.Printf("%c", v)輸出字元。這些defer`呼叫將被新增到棧中並以後進先出的順序執行,因此字串將以相反的順序列印。該程式將輸出,

Orignal String: Naveen
Reversed String: neevaN
複製程式碼

defer的實際用法

到目前為止我們看到的程式碼示例沒有顯示defer的實際用法。在本節中,我們將研究defer的一些實際用途。

defer用於應該執行函式呼叫的地方,而不管程式碼流程如何???。讓我們用一個使用WaitGroup的例子來理解這一點。我們將首先編寫程式而不使用defer,然後我們將修改它以使用defer,以此來理解defer是多麼有用。

package main

import (
    "fmt"
    "sync"
)

type rect struct {
    length int
    width  int
}

func (r rect) area(wg *sync.WaitGroup) {
    if r.length < 0 {
        fmt.Printf("rect %v's length should be greater than zero\n", r)
        wg.Done()
        return
    }
    if r.width < 0 {
        fmt.Printf("rect %v's width should be greater than zero\n", r)
        wg.Done()
        return
    }
    area := r.length * r.width
    fmt.Printf("rect %v's area %d\n", r, area)
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    r1 := rect{-67, 89}
    r2 := rect{5, -67}
    r3 := rect{8, 9}
    rects := []rect{r1, r2, r3}
    for _, v := range rects {
        wg.Add(1)
        go v.area(&wg)
    }
    wg.Wait()
    fmt.Println("All go routines finished executing")
}
複製程式碼

Run in playground 在上面的程式中,我們在第 8 行建立了一個rect結構,第 13 行給rect結構加上了area方法用於計算矩形的面積。此方法檢查矩形的長度和寬度是否小於 0。如果是這樣,它會列印相應的訊息,否則會列印矩形的面積。

main函式建立了 3 個型別為rect的變數r1r2r3,將它們新增到rects切片中。然後使用for range迴圈迭代該切片,並將area方法併發執行。 WaitGroup wg用於保證所有Goroutines執行完畢。WaitGroup作為引數傳遞給area方法,並在area方法中呼叫wg.Done,主要通知mainGoroutine已完成其工作。如果您仔細觀察,可以看到這些呼叫恰好在area方法返回之前發生。無論程式碼採用哪個條件分支執行,都應在方法返回之前呼叫wg.Done,因此可以通過defer來解決這種場景。

來用defer重寫上面的程式吧。

在下面的程式中,我們刪除了上面程式中的 3 個wg.Done,並將其替換為defer wg.Done(),這將使程式碼更簡潔易懂。

package main

import (
    "fmt"
    "sync"
)

type rect struct {
    length int
    width  int
}

func (r rect) area(wg *sync.WaitGroup) {
    defer wg.Done()
    if r.length < 0 {
        fmt.Printf("rect %v's length should be greater than zero\n", r)
        return
    }
    if r.width < 0 {
        fmt.Printf("rect %v's width should be greater than zero\n", r)
        return
    }
    area := r.length * r.width
    fmt.Printf("rect %v's area %d\n", r, area)
}

func main() {
    var wg sync.WaitGroup
    r1 := rect{-67, 89}
    r2 := rect{5, -67}
    r3 := rect{8, 9}
    rects := []rect{r1, r2, r3}
    for _, v := range rects {
        wg.Add(1)
        go v.area(&wg)
    }
    wg.Wait()
    fmt.Println("All go routines finished executing")
}
複製程式碼

Run in palyground 輸出,

rect {8 9}'s area 72
rect {-67 89}'s length should be greater than zero
rect {5 -67}'s width should be greater than zero
All go routines finished executing
複製程式碼

defer不僅能讓程式簡潔使用,在上述例子還有一個優點。假設我們使用新的if條件向area方法新增另一個處理分支。如果沒有deferwg.Done的呼叫,我們必須小心確保在這個新的處理分支中呼叫wg.Done。但由於對wg.Done的呼叫用了defer,我們再也不用擔心這種情況了。相似的應用場景應該還有很多,比如開啟檔案的關閉等等。但是需要注意的是,大量的使用defer函式會導致程式執行效率變低。

相關文章