4.1.3 作用域
上一节介绍了闭包函数,大家已经知道了它可以让变量的作用域限制失效,但是如果对函数当中变量的作用域进行深入分析,你会发现有些情况下过多使用闭包函数会让我们在编程当中遇到麻烦。来看一下示例代码:
book/ch04/4.1/clo2/main.go
1. package main
2.
3. import "fmt"
4.
5. func main() {
6. //方式一
7. var funcList []func()
8. for i:=0;i<3;i++{
9. funcList = append(funcList,func(){
10. fmt.Println(i)
11. })
12. }
13. for _,f := range funcList{
14. f()
15. }
16. //方式二
17. var funcList2 []func()
18. for i:=0;i<3;i++{
19. j := i
20. funcList2 = append(funcList2,func(){
21. fmt.Println(j)
22. })
23. }
24. for _,f := range funcList2{
25. f()
26. }
27. //方式三
28. var funcList3 []func(int)
29. for i:=0;i<3;i++{
30. funcList3 = append(funcList3,func(i int){
31. fmt.Println(i)
32. })
33. }
34. for i,f := range funcList3{
35. f(i)
36. }
37. }
38.
39. //以下是程序运行结果
40. 3
41. 3
42. 3
43. 0
44. 1
45. 2
46. 0
47. 1
48. 2
第7行至第15行,先在第7行定义了一个函数的切片,然后用一个for循环为切片增加了三个匿名函数,每个匿名函数的函数体内都只有一个打印语句,其作用是把for循环里面的变量i打印出来。第40行至第42行是打印结果,可以看到打印了三个3,而不是0、1、2。
第17行至第26行,先定义了一个函数的切片,第17行和第7行的两个切片类型完全一样,仅仅是切片的名称不一样。注意,第19行不是直接让匿名函数打印for循环中的变量i,而是先把i赋值给j,由匿名函数来打印j。第43行至第45行是打印结果,可以看到结果为0、1、2。
下面先来分析一下这两种方式为什么会有不同的结果。其实很简单,前文已经强调过,匿名函数使用外部变量用的是指针,并非值复制。在for循环里变量i是一个共享变量,每循环一次都会把原来存储的值加1。三次循环执行完后,i存储的就是3,第一种方式在函数执行的时候虽然打印了3次,也只是重复打印了三次3。而第二种方式在循环体内每次执行的时候又声明了一个局部变量j,相当于每次都把i不同的值存储下来,最终输出结果就是0、1、2。
说明
Go语言里的局部变量名称和全局变量名称是可以重复的,重复的时候,系统会默认把重名变量看作局部变量。所以,上面的方式二中,第19行完全可以写成i := i,下面的打印语句也使用i,结果还会是0、1、2,因为编译器是可以区分两个i的。但这样写显然会给阅读和程序检查带来困扰,所以不建议这样写代码。
第28行至第37行,方式三是方式二的简单变形,如果觉得单独增加一个局部变量会比较突兀,也可以通过改变函数类型的方式来增加形参,这样一来,不需要显式地写局部变量声明也可以达到打印0、1、2的目的。
注意
闭包会将自己用到的变量都保存在内存中,导致变量无法被及时回收,并且可能通过闭包修改父函数使用的变量值,所以在使用的时候也要注意性能和安全。