Golang for...range和闭包Closure
什么是闭包?
闭包:一个可以使用另外一个函数作用域中的变量 的函数。
用一个专业一点的说法就是:函数调用返回后一个没有释放资源的栈区;
从功能性上说lambda和closure(或是OC中的blocks)是一个东西,只是不同语言的不同称呼罢了,它们都是匿名函数。若匿名函数捕获了一个外部变量,那么它就是一个closure(闭包)。
一般,当函数执行完毕后,局部活动对象会被销毁,内存中仅保存全局作用域,但闭包的情况是不一样的。闭包的活动对象依然会保存在内存中,于是像上例中,函数调用返回后,变量i是属于活动对象里面的,就是说其栈区还没有释放,但你调用c()的时候i变量保存的作用域链从b()->a()->全局去寻找作用域var i声明所在,然后找到了var i=1;然后在闭包内++i;结果,最后输出的值就是2了;不知道这么说有没人明白,如果不明白,那么只要记住它的闭包的两个点就好了,一点就是闭包的活动对象没有被销毁;第二点是作用域链的关键是他要遇到var 声明;就好了····
共同点:他们都有是函数;除此之外没有任何共同点;
今天遇到了一个有趣的问题,涉及到了golang编程的三个知识点,for...range
、闭包
和goroutine
为了简化问题,写了一段示例代码来做分析:
package main
import "time"
func main() {
ss := []string{
"haha",
"hehe",
"xixi",
}
for _, s := range ss {
go func() {
print(s, "\n")
}()
}
time.Sleep(1 * time.Second)
}
如果我们无意中写出这种代码,心里预期的输出可能是三个不同的字符串,当然,顺序可能是不固定的,这是goroutine
的特性决定的,我们这里就不做说明了,但实际上着代码的输出会是
xixi
xixi
xixi
如果我们将代码修改为
package main
import "time"
func main() {
ss := []string{
"haha",
"hehe",
"xixi",
}
for _, s := range ss {
go func(s1 string) {
print(s1, "\n")
}(s)
// time.Sleep(1 * time.Microsecond)
}
time.Sleep(1 * time.Second)
}
则可以得到我们期望的输出
haha
hehe
xixi
原因在于for...range
的赋值方式和闭包
概念:
fro...range
The iteration variables may be declared by the "range" clause using a form of short
variable declaration (:=). In this case their types are set to the types of the
respective iteration values and their scope is the block of the "for" statement; they
are re-used in each iteration. If the iteration variables are declared outside the "for"
statement, after execution their values will be those of the last iteration.
这是goang
官方对for...range
中迭代变量--也就是我们代码中的s
--的说明
也就是说
for _, s := range ss {
//TODO:
}
和
var s string
for _, s = range ss {
//TODO:
}
这两种写法是同样的效果,但是第二种写法相对来说就很好理解--for...range
的迭代中不会重复分配迭代变量,而是会重复利用第一次生成的或者传入的迭代变量,for...range
还有另外一个坑,在此不做赘述
闭包
简单点讲,引用了全局变量的函数就是典型的闭包
1,这个函数在不同场景调用会产生不同的效果(即使收到了同样的输入参数),也可以称之为闭包在运行时可以有多个实例
所以我们第二段代码中,通过给匿名函数增加了一个输入变量,这样虽然我们仍在使用for...range
,但是没有了闭包
,出现一个传值调用的过程,最终得到我们期望的输出
闭包概述
闭包(Closure)是词法闭包(Lexical Closure)的简称,是为了解决"函数的引用环境可能发生变化"这一问题 而引入的特性。闭包的概念早在高级语言开始发展的年代就产生了,比较靠谱的定义,为: 闭包,是由函数和引用环境(即变量)组合而成的实体,是附有数据的行为1。
不同的语言,对闭包的支持形式不尽相同,但基本思想都是一致的。当编程语言满足下述条件时,可以较好的支持闭包特性:
- 函数是一阶值(First-class value),即函数可以作为变量、可以作为另一个函数的返回值或参数
- 支持函数的嵌套
- 引用环境和函数组合而成的实体,可以被调用
- 支持匿名函数
闭包优雅的处理了一些棘手的问题,如提高了代码的抽象程度、精简代码等等。但由于引用环境的问题,闭包也会带来了很多的副作用,这正是下文的主题:-D
Golang的闭包
Golang支持匿名函数,这个功能很好的支持了闭包特性。举个例子,来说明Golang闭包的典型做法,如下,
package main
import "fmt"
func intSeq() func() int {
i := 0
return func() int {
i += 1
fmt.Printf("%d, %d\n", i, &i)
return i
}
}
func main() {
nextInt := intSeq()
nextInt()
nextInt()
nextInt()
fmt.Println()
newInts := intSeq()
newInts()
}
运行结果,如下(将上述代码,保存为文件closures.go
),
$ go run closures.go
1, 0xc20800a200
2, 0xc20800a200
3, 0xc20800a200
1, 0xc20800a288
函数intSeq
,定义了一个匿名函数,并把它作为返回值。作为返回值的函数,捕获了一个内部引用变量i
,从而构成了一个闭包。
在main
函数中,我们首先定义了一个函数变量nextInt
。nextInt
会捕获变量i
的取值现场(0)、另存为自己的私有数据(初始值为0),后续每次调用nextInt
时 都会更新其私有数据内容(0 -> 1 -> 2 -> 3)、私有数据的地址不发生变化(始终为0xc20800a200
)。
每次调用intSeq
产生的闭包实例,其引用环境都是私有的、不受其他实例影响的。从newInts
上面,可以验证上述结论:newInts
的私有数据地址为0xc20800a288
、取值不受nextInt
影响。
Golang闭包的副作用
闭包带来了代码层面的灵活性,却也因为引用导致了一些副作用。特别的,当对引用环境理解不够、处理不当时,会产生意想不到的错误。下面,就是我采过的坑:-(
func pit() {
for i := 0; i < 3; i++ {
go func() {
fmt.Printf("%d ", i)
}()
}
// Output: 3 3 3
}
上面的代码,我们期盼的输出为0 1 2
,结果却是3 3 3
,这就是闭包带来的副作用。回避这个副作用的办法,有(1)通过函数的参数传值 或(2)在闭包内构造临时变量:
// 方法1: 通过函数的参数传值
func pitFix1() {
for i := 0; i < 3; i++ {
go func(num int) {
fmt.Printf("%d ", num)
}(i)
}
// Output: 0 1 2
}
// 方法2: 在闭包内构造临时变量i
func pitFix2() {
for i := 0; i < 3; i++ {
i := i
go func() {
fmt.Printf("%d ", i)
}()
}
// Output: 0 1 2
}
小结
如"闭包"一样的语言特性,带来灵活性的同时 往往伴随着一个个的坑。没有完美的设计。