Go语言基础之defer语句
Go语言基础之defer语句
Go语言中的defer
语句会将其后面跟随的语句进行延迟处理。在defer
归属的函数即将返回时,将延迟处理的语句按defer
定义的逆序进行执行,也就是说,先被defer
的语句最后被执行,最后被defer
的语句,最先被执行。
举个例子:
func main() {
fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("end")
}
输出结果:
start
end
3
2
1
由于defer
语句延迟调用的特性,所以defer
语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。
defer执行时机
在Go语言的函数中return
语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer
语句执行的时机就在返回值赋值操作后,RET指令执行前。具体如下图所示:
实参取值(Arguments Evaluation)
在 Go 语言中,并非在调用延迟函数的时候才确定实参,而是当执行 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)
}
在上面的程序里的第 11 行,a
的初始值为 5。在第 12 行执行 defer
语句的时候,由于 a
等于 5,因此延迟函数 printA
的实参也等于 5。接着我们在第 13 行将 a
的值修改为 10。下一行会打印出 a
的值。该程序输出:
value of a before deferred function call 10
value of a in deferred function 5
从上面的输出,我们可以看出,在调用了 defer
语句后,虽然我们将 a
修改为 10,但调用延迟函数 printA(a)
后,仍然打印的是 5。
defer经典案例
阅读下面的代码,写出最后的打印结果。
func f1() int {
x := 5
defer func() {
x++
}()
return x
}
func f2() (x int) {
defer func() {
x++
}()
return 5
}
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x
}
func f4() (x int) {
defer func(x int) {
x++
}(x)
return 5
}
func main() {
fmt.Println(f1())
fmt.Println(f2())
fmt.Println(f3())
fmt.Println(f4())
}
5
6
5
5
defer 的实际应用
目前为止,我们看到的代码示例,都没有体现出 defer
的实际用途。本节我们会看看 defer
的实际应用。
当一个函数应该在与当前代码流(Code Flow)无关的环境下调用时,可以使用 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")
}
在上面的程序里,我们在第 8 行创建了 rect
结构体,并在第 13 行创建了 rect
的方法 area
,计算出矩形的面积。area
检查了矩形的长宽是否小于零。如果矩形的长宽小于零,它会打印出对应的提示信息,而如果大于零,它会打印出矩形的面积。
main
函数创建了 3 个 rect
类型的变量:r1
、r2
和 r3
。在第 34 行,我们把这 3 个变量添加到了 rects
切片里。该切片接着使用 for range
循环遍历,把 area
方法作为一个并发的 Go 协程进行调用(第 37 行)。我们用 WaitGroup wg
来确保 main
函数在其他协程执行完毕之后,才会结束执行。WaitGroup
作为参数传递给 area
方法后,在第 16 行、第 21 行和第 26 行通知 main
函数,表示现在协程已经完成所有任务。如果你仔细观察,会发现 wg.Done() 只在 area 函数返回的时候才会调用。wg.Done() 应该在 area 将要返回之前调用,并且与代码流的路径(Path)无关,因此我们可以只调用一次 defer,来有效地替换掉 wg.Done() 的多次调用。
我们来用 defer
来重写上面的代码。
在下面的代码中,我们移除了原先程序中的 3 个 wg.Done
的调用,而是用一个单独的 defer wg.Done()
来取代它(第 14 行)。这使得我们的代码更加简洁易懂。
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
方法添加了一条返回路径(Return Path)。如果没有使用 defer
来调用 wg.Done()
,我们就得很小心了,确保在这条新添的返回路径里调用了 wg.Done()
。由于现在我们延迟调用了 wg.Done()
,因此无需再为这条新的返回路径添加 wg.Done()
了。
defer面试题
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
x := 1
y := 2
defer calc("AA", x, calc("A", x, y))
x = 10
defer calc("BB", x, calc("B", x, y))
y = 20
}
问,上面代码的输出结果是?(提示:defer注册要延迟执行的函数时该函数所有的参数都需要确定其值)
A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4