原文连接:Go 语言闭包详解html
什么是闭包?闭包是由函数和与其相关的引用环境组合而成的实体。golang
下面就来经过几个例子来讲明 Go 语言中的闭包以及由闭包引用产生的问题。闭包
在说明闭包以前,先来了解一下什么是函数变量。app
在 Go 语言中,函数被看做是第一类值,这意味着函数像变量同样,有类型、有值,其余普通变量能作的事它也能够。函数
func square(x int) { println(x * x) }
square(1)
s := square
;接着能够调用这个函数变量:s(1)
。square
后面没有圆括号,调用才有。 nil
的函数变量会致使 panic。nil
,这意味着它能够跟 nil
比较,但两个函数变量之间不能比较。如今开始经过例子来讲明闭包:post
func incr() func() int { var x int return func() int { x++ return x } }
调用这个函数会返回一个函数变量。spa
i := incr()
:经过把这个函数变量赋值给 i
,i
就成为了一个闭包。指针
因此 i
保存着对 x
的引用,能够想象 i 中有着一个指针指向 x 或 i 中有 x 的地址。code
因为 i
有着指向 x
的指针,因此能够修改 x
,且保持着状态:htm
println(i()) // 1 println(i()) // 2 println(i()) // 3
也就是说,x
逃逸了,它的生命周期没有随着它的做用域结束而结束。
可是这段代码却不会递增:
println(incr()()) // 1 println(incr()()) // 1 println(incr()()) // 1
这是由于这里调用了三次 incr()
,返回了三个闭包,这三个闭包引用着三个不一样的 x
,它们的状态是各自独立的。
如今开始经过例子来讲明由闭包引用产生的问题:
x := 1 f := func() { println(x) } x = 2 x = 3 f() // 3
由于闭包对外层词法域变量是引用的,因此这段代码会输出 3。
能够想象 f
中保存着 x
的地址,它使用 x
时会直接解引用,因此 x
的值改变了会致使 f
解引用获得的值也会改变。
可是,这段代码却会输出 1:
x := 1 func() { println(x) // 1 }() x = 2 x = 3
把它转换成这样的形式就容易理解了:
x := 1 f := func() { println(x) } f() // 1 x = 2 x = 3
这是由于 f
调用时就已经解引用取值了,这以后的修改就与它无关了。
不过若是再次调用 f
仍是会输出 3,这也再一次证实了 f
中保存着 x
的地址。
能够经过在闭包内外打印所引用变量的地址来证实:
x := 1 func() { println(&x) // 0xc0000de790 }() println(&x) // 0xc0000de790
能够看到引用的是同一个地址。
接下来在三个例子中说明由循环内的闭包引用所产生的问题:
for i := 0; i < 3; i++ { func() { println(i) // 0, 1, 2 }() }
这段代码至关于:
for i := 0; i < 3; i++ { f := func() { println(i) // 0, 1, 2 } f() }
每次迭代后都对 i
进行了解引用并使用获得的值且再也不使用,因此这段代码会正常输出。
正常代码:输出 0, 1, 2:
var dummy [3]int for i := 0; i < len(dummy); i++ { println(i) // 0, 1, 2 }
然而这段代码会输出 3:
var dummy [3]int var f func() for i := 0; i < len(dummy); i++ { f = func() { println(i) } } f() // 3
前面讲到闭包取引用,因此这段代码应该输出 i 最后的值 2 对吧?
不对。这是由于 i
最后的值并非 2。
把循环转换成这样的形式就容易理解了:
var dummy [3]int var f func() for i := 0; i < len(dummy); { f = func() { println(i) } i++ } f() // 3
i
自加到 3 才会跳出循环,因此循环结束后 i
最后的值为 3。
因此用 for range
来实现这个例子就不会这样:
var dummy [3]int var f func() for i := range dummy { f = func() { println(i) } } f() // 2
这是由于 for range
和 for
底层实现上的不一样。
var funcSlice []func() for i := 0; i < 3; i++ { funcSlice = append(funcSlice, func() { println(i) }) } for j := 0; j < 3; j++ { funcSlice[j]() // 3, 3, 3 }
输出序列为 3, 3, 3。
看了前面的例子以后这里就容易理解了:
这三个函数引用的都是同一个变量(i
)的地址,因此以后 i
递增,解引用获得的值也会递增,因此这三个函数都会输出 3。
添加输出地址的代码能够证实:
var funcSlice []func() for i := 0; i < 3; i++ { println(&i) // 0xc0000ac1d0 0xc0000ac1d0 0xc0000ac1d0 funcSlice = append(funcSlice, func() { println(&i) }) } for j := 0; j < 3; j++ { funcSlice[j]() // 0xc0000ac1d0 0xc0000ac1d0 0xc0000ac1d0 }
能够看到三个函数引用的都是 i
的地址。
j := i
,且把以后对 i
的操做改成对 j
操做。i := i
。注意:这里短声明右边是外层做用域的 i
,左边是新声明的做用域在这一层的 i
。原理同上。这至关于为这三个函数各声明一个变量,一共三个,这三个变量初始值分别对应循环中的 i
而且以后不会再改变。
var funcSlice []func() for i := 0; i < 3; i++ { func(i int) { funcSlice = append(funcSlice, func() { println(i) }) }(i) } for j := 0; j < 3; j++ { funcSlice[j]() // 0, 1, 2 }
如今 println(i)
使用的 i
是经过函数参数传递进来的,而且 Go 语言的函数参数是按值传递的。
因此至关于在这个新的匿名函数内声明了三个变量,被三个闭包函数独立引用。原理跟第一种方法是同样的。
这里的解决方法能够用在大多数跟闭包引用有关的问题上,不局限于第三个例子。