Defer

什么是defer

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)
}

这段程序比较简单,找到一个切片的最大值并打印。注意函数largest的第一行语句defer finished(),会推迟函数finished()的执行。defer所在的函数为largest,因此推迟到largest函数体的最后,即return之前。该程序输出如下:

Started finding largest
Largest number in [78 109 2 563 300] is 563
Finished finding largest

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 ")
}

这里我们推迟了一个方法的执行defer p.fullName()。该程序比较简单,输出如下:

Welcome John Smith

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)
}

这里a的初始值为5,然后执行defer语句,将a值传递给被defer的函数printA,这时入参就已经确定了,即便后面我们又把a的值更新为10printA执行时的入参仍然是5。该程序输出如下:

value of a before deferred function call 10
value of a in deferred function 5

defer栈

如果一个函数内同时出现了多个defer,那么这些defer是按照栈结构来执行的,即先defer的后执行,后defer的先执行的顺序。下面我们通过这个特性来将一个字符串反转下:

package main

import "fmt"

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

这里的for range循环遍历字符串的每个字符,执行defer fmt.Printf("%c", v)语句,由于被defer的函数是栈顺序,因此打印的字符串是反转过来的。该程序输出结果为:

Original String: Naveen
Reversed String: neevaN

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")
}

这里声明了结构体类型rect,即长方形类型,并在该结构体类型上定义了方法area用来计算面积。该方法内需要判断入参的合法性,即是否都是非负数。合法性判断完成后才打印输出长方形的面积。area方法内部有三个分支,r.length < 0r.width < 0以及正常情况。但是每个分支在执行完对应逻辑时都需要调用wg.Done()来通知协程执行完毕,接着调用return退出方法的执行。这种场景非常适合用defer,因为我们无论执行哪个分支都需要在return前调用wg.Done(),因此可以将所有分支中的wg.Done()抽出来放到defer语句中。这样代码可读性更高,而且容易维护。 下面我们用defer重构下:

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")
}

该程序输出如下:

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

这里使用defer还有另外一个好处,假设我们后面又新增了一个判断分支,如果不是用defer,那么我们还得留心加上语句wg.Done()。因此这种场景还是使用defer更好,代码的可读性、可扩展性都比较好。

Last updated