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
函数将int slice
作为参数,并输出最大数量的输入切片。largest
函数的第一行包含语句defer finished()
。 这意味着在largest
函数返回之前将调用finished()
函数。 运行此程序,您可以看到以下输出打印。
Started finding largest
Largest number in [78 109 2 563 300] is 563
Finished finding largest
largest
函数开始执行并打印上述输入的前两行。 在它可以返回之前,我们的延迟函数finished
执行并打印文本完成查找最大:)
延迟方法
延迟不仅限于函数。 延迟方法调用也是完全合法的。 让我们写一个小程序来测试它。
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 ")
}
在上面的程序中,我们延迟了第22行中的方法调用。 该方案的其余部分是不言自明的。 该程序输出,
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)
}
在上面的程序中,a初始化为5。当在第12行中执行延迟语句时,a的值是5,因此这将是延迟的printA函数的参数。 我们将a的值更改为第10行,下一行打印a的值。 该程序输出,
value of a before deferred function call 10
value of a in deferred function 5
从上面的输出可以理解,尽管在执行延迟语句之后a的值变为10,但实际的延迟函数调用printA(a)仍然打印5。
栈的延迟
当一个函数有多个延迟调用时,它们会被添加到堆栈中并以后进先出(LIFO)顺序执行。
我们将编写一个小程序,使用一堆延迟来反向打印字符串。
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)
}
}
在上面的程序中,for循环迭代字符串并调用第12行中的fmt.Printf(“%c”,v)。这些延迟调用将被添加到堆栈中并以后进先出顺序执行,因此字符串将以相反的顺序打印。 该程序将输出,
Orignal String: Naveen
Reversed String: neevaN
延迟使用
到目前为止我们看到的代码示例没有显示defer
的实际用法。 在本节中,我们将研究延迟的一些实际用途。
Defer
用于应该执行函数调用的地方,而不管代码流程如何。 让我们用一个使用WaitGroup的程序的例子来理解这一点。 我们将首先编写程序而不使用延迟,然后我们将修改它以使用延迟并理解延迟是多么有用。
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
方法计算矩形的面积。此方法检查矩形的长度和宽度是否小于零。如果是这样,它会打印相应的消息,否则会打印矩形的面积。
main
函数创建了3个类型为rect的变量r1
,r2
和r3
。然后将它们添加到rects
切片中。然后使用for range
循环迭代该切片,并将area
方法称为当前并发Goroutine。WaitGroup wg
用于确保主函数被阻止,直到所有Goroutines完成执行。此WaitGroup作为参数传递给area
方法,area
方法中调用wg.Done()
通知主函数Goroutine已完成其工作。如果您仔细注意,可以看到这些调用恰好在area
方法返回之前发生。无论代码流采用何种路径,都应在方法返回之前调用wg.Done()
,因此可以通过单个延迟调用有效地替换这些调用。
让我们使用defer重写上面的程序。
在下面的程序中,我们删除了上述程序中的3个wg.Done()
调用,并将其替换为第14行中的单个延迟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
在上述程序中使用延迟还有一个优点。 假设我们使用新的if
条件向area
方法添加另一个返回路径。 如果没有延迟对wg.Done()
的调用,我们必须小心并确保在这个新的返回路径中调用wg.Done()
。 但由于对wg.Done()
的调用被推迟,我们不必担心为此方法添加新的返回路径。