什么是 Defer?
Defer 语句用于存在 defer 语句的函数返回之前执行函数调用。定义可能看起来很复杂,但通过一个例子来理解它很简单。
Example
package mainimport ("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 largestLargest number in [78 109 2 563 300] is 563Finished finding largest
largest 函数开始执行并输出上述输入的前两行。在它可以返回之前,我们的延迟函数完成执行并输出 Finished finding largest:)
延迟方法
Defer 不仅限于函数。调用延迟方法也是完全合法的。让我们编写一个小程序来测试它。
package mainimport ("fmt")type person struct {firstName stringlastName 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 mainimport ("fmt")func printA(a int) {fmt.Println("value of a in deferred function", a)}func main() {a := 5defer printA(a)a = 10fmt.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 10value of a in deferred function 5
从上面的输出可以理解,尽管在执行延迟语句之后 a 的值变为 10,但实际的延迟函数调用 printA(a) 仍然输出 5。
defer 的堆栈
当一个函数有多个延迟调用时,它们会被添加到堆栈中并以后进先出(LIFO)顺序执行。
我们将编写一个小程序,使用一堆 defer 来反向输出字符串。
package mainimport ("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 调用将被添加到堆栈中。

上图表示的是添加 defer 调用后的堆栈内容。栈)是一个后进先出的数据结构。最后被推到栈中的 defer 调用将被弹出并首先执行。在这种情况下,defer fmt.Printf(“%c”, ‘n’) 将被先执行,因此字符串将以相反的顺序打印。
Original String: NaveenReversed String: neevaN
defer 的实际用途
到目前为止我们看到的代码示例没有 defer 的实际用法。在本节中,我们将研究 defer的一些实际用途。
Defer 用于应该执行函数调用的地方,而不管代码流程如何。让我们用一个使用WaitGroup 的程序的例子来理解这一点。我们将首先编写程序而不使用 Defer,然后我们将修改它以使用 Defer 并理解延迟是多么有用。
package mainimport ("fmt""sync")type rect struct {length intwidth 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.widthfmt.Printf("rect %v's area %d\n", r, area)wg.Done()}func main() {var wg sync.WaitGroupr1 := 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 mainimport ("fmt""sync")type rect struct {length intwidth 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.widthfmt.Printf("rect %v's area %d\n", r, area)}func main() {var wg sync.WaitGroupr1 := 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 72rect {-67 89}'s length should be greater than zerorect {5 -67}'s width should be greater than zeroAll go routines finished executing
在上述程序中使用 defer 还有一个优点。假设我们使用新的 if 条件向 area 方法添加另一个返回路径。如果没有 defer 对 wg.Done()的调用,我们必须小心并确保在这个新的返回路径中调用 wg.Done()。但由于对 wg.Done() 的调用被推迟,我们不必担心为此方法添加新的返回路径。
