内容简介:什么是闭包?下面就来通过几个例子来说明 Go 语言中的闭包以及由闭包引用产生的问题。在说明闭包之前,先来了解一下什么是
什么是闭包? 闭包是由函数和与其相关的引用环境组合而成的实体。
下面就来通过几个例子来说明 Go 语言中的闭包以及由闭包引用产生的问题。
函数变量(函数值)
在说明闭包之前,先来了解一下什么是 函数变量 。
在 Go 语言中,函数被看作是 第一类值 ,这意味着函数像变量一样,有类型、有值,其他普通变量能做的事它也可以。
func square(x int) { println(x * x) } 复制代码
- 直接调用:
square(1)
- 把函数当成变量一样赋值:
s := square
;接着可以调用这个函数变量:s(1)
。 注意:这里square
后面没有圆括号,调用才有。
- 调用
nil
的函数变量会导致 panic。 - 函数变量的零值是
nil
,这意味着它可以跟nil
比较,但两个函数变量之间不能比较。
闭包
现在开始通过例子来说明闭包:
func incr() func() int { var x int return func() int { x++ return x } } 复制代码
调用这个函数会返回一个函数变量。
i := incr()
:通过把这个函数变量赋值给 i
, i
就成为了一个 闭包 。
所以 i
保存着对 x
的引用,可以想象 i 中有着一个指针指向 x 或 i 中有 x 的地址 。
由于 i
有着指向 x
的指针,所以可以修改 x
,且保持着状态:
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
的地址。
解决方法
1. 声明新变量:
- 声明新变量:
j := i
,且把之后对i
的操作改为对j
操作。 - 声明新同名变量:
i := i
。 注意:这里短声明右边是外层作用域的i
,左边是新声明的作用域在这一层的i
。原理同上。
这相当于为这三个函数各声明一个变量,一共三个,这三个变量初始值分别对应循环中的 i
并且之后不会再改变。
2. 声明新匿名函数并传参:
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 语言的函数参数是按值传递的。
所以相当于在这个新的匿名函数内声明了三个变量,被三个闭包函数独立引用。原理跟第一种方法是一样的。
这里的解决方法可以用在大多数跟闭包引用有关的问题上,不局限于第三个例子。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 史上最强 JavaScript 闭包原理详解:从入门到放弃
- 草根学Python(十五) 闭包(解决一个需求了解闭包流程)
- [原]谈一谈闭包
- Java闭包如何工作
- 理解 JavaScript 闭包
- 管窥python闭包
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。