[译] part 29: golang defer

617 阅读5分钟

什么是 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函数会导致程序运行效率变低。