什么是 Defer?
Defer 语句用于存在 defer 语句的函数返回之前执行函数调用。定义可能看起来很复杂,但通过一个例子来理解它很简单。
Example
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
函数将 int 切片作为参数,并输出输入切片的最大数字。largest
函数的第一行包含语句defer finished()
。这意味着在 largest
函数返回之前将调用 finished()
函数。运行此程序,你可以看到以下输出。
Started finding largest
Largest number in [78 109 2 563 300] is 563
Finished finding largest
largest 函数开始执行并输出上述输入的前两行。在它可以返回之前,我们的延迟函数完成执行并输出 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 ")
}
在上面的程序中我们在 21 行调用了延迟方法,其余代码都一目了然。程序输出
Welcome John Smith
参数评估
延迟函数的参数在执行 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)
}
在上面的程序中,在 12 行 a
最初的值为 5
。 在 13 行执行 defer 语句时,a 的值为5,因此这将是 printA
函数的延迟参数。 我们在 14 行将 a
的值更改为 10。 下一行输出 a
的值。 该程序输出,
value of a before deferred function call 10
value of a in deferred function 5
从上面的输出可以理解,尽管在执行延迟语句之后 a
的值变为 10
,但实际的延迟函数调用 printA(a)
仍然输出 5
。
defer 的堆栈
当一个函数有多个延迟调用时,它们会被添加到堆栈中并以后进先出(LIFO)顺序执行。
我们将编写一个小程序,使用一堆 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)
}
}
在上面的程序中,在第 11 行的 for range 循环中,迭代字符串,并在第 12 行调用 defer fmt.Printf(“%c”, v)。这些 defer 调用将被添加到堆栈中。
![image.png](https://cdn.nlark.com/yuque/0/2020/png/137440/1598669161628-a9bf4f99-5cae-47dd-93f9-faac0bb30ac6.png#align=left&display=inline&height=281&margin=%5Bobject%20Object%5D&name=image.png&originHeight=281&originWidth=181&size=12075&status=done&style=none&width=181)
上图表示的是添加 defer 调用后的堆栈内容。栈)是一个后进先出的数据结构。最后被推到栈中的 defer 调用将被弹出并首先执行。在这种情况下,defer fmt.Printf(“%c”, ‘n’) 将被先执行,因此字符串将以相反的顺序打印。
Original String: Naveen
Reversed String: neevaN
defer 的实际用途
到目前为止我们看到的代码示例没有 defer 的实际用法。在本节中,我们将研究 defer的一些实际用途。
Defer 用于应该执行函数调用的地方,而不管代码流程如何。让我们用一个使用WaitGroup 的程序的例子来理解这一点。我们将首先编写程序而不使用 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")
}
在上面的程序中,我们在第 8 行创建了一个rect
结构,在第 13 行创建了一个方法 area
,用于计算矩形的面积。 此方法检查矩形的长度和宽度是否小于零。 如果是这样,它会输出相应的消息,否则会输出矩形的区域。
main
函数创建了 3 个类型为 rect
的变量 r1
, r2
和 r3
。 然后在 34 行将它们添加到rects
切片中。 然后使用 for range
循环迭代该切片,在 37 行并将 area
方法被调用成并发的 Goroutine。WaitGroup wg
用于确保主函数被阻塞,直到所有 Goroutines 完成执行。 WaitGroup 作为参数传递给 area 方法,在 16、21、26 行 area 方法调用wg.Done()
以通知 main 函数 Goroutine 完成其任务。 你可以注意到,这些调用恰好在 area 方法返回之前发生。 无论代码流采用何种路径,都应在方法返回之前调用 wg.Done(),因此可以通过单个 defer
有效地替换这些调用。
让我们使用 defer 重写上面的程序。
在下面的程序中,我们删除了上面程序中的 3 个 wg.Done()
调用,并将其替换为第 14 行中的单个 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")
}
该程序输出,
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
方法添加另一个返回路径。如果没有 defer 对 wg.Done()
的调用,我们必须小心并确保在这个新的返回路径中调用 wg.Done()
。但由于对 wg.Done()
的调用被推迟,我们不必担心为此方法添加新的返回路径。