函数变量(函数值)
在 Go 语言中,函数被看作是第一类值,这意味着函数像变量一样,有类型、有值,其他普通变量能做的事它也可以。
1 | func square(x int) { |
- 直接调用:square(1)
- 把函数当成变量一样赋值:
s := square
;接着可以调用这个函数变量:s(1)。 注意:这里 square 后面没有圆括号,调用才有。
- 调用
nil
的函数变量会导致 panic。 - 函数变量的零值是 nil,这意味着它可以跟 nil 比较,但两个函数变量之间不能比较。
匿名函数
作用: 在go语言中目前了解的作用就是用于构成闭包
闭包
闭包通过引用的方式使用外部函数的变量
函数与 与其(直接)相关的环境形成闭包
简单来说: 因为把返回的函数赋给了一个变量, 虽然函数在执行完一瞬间会销毁其执行环境, 但是如果有闭包的话, 闭包会保存外部函数的活动对象(变量), 所以如果不对闭包的引用消除掉, 闭包会一直存在内存中, 垃圾收集器不会销毁闭包占用的内存
实例1
1 | //函数A是一个不带参数,返回值是一个匿名函数,且该函数 |
调用1:
1 | func main() { |
调用2
1 | func main() { |
以上调用通过闭包实现了sum的累加
调用3
1 | func main() { |
可以看出,上例中调用了两次函数A,构成了两个闭包,这两个闭包维护的变量sum不是同一个变量。
实例2
1 | func B() []func() { |
闭包通过引用的方式使用外部函数的变量。
上例中只调用了一次函数B,构成一个闭包(func() {fmt.Println(i)}
与它的环境func B() []func(){}
构成闭包),i 在外部函数B中定义,所以闭包维护该变量 i ,c[0]、c[1]、c[2]中的 i 都是闭包中 i 的引用。
因此执行c:=B()
后,i 的值已经变为3,故再调用c0时的输出是3而不是0。
可作如下修改:
1 | func B() []func() { |
函数func() {fmt.Println(j)}
与它的环境func(j int) func() {}
构成闭包, 变量i
(实参) 并没有在它的环境范围内, 且 j
是形参
以上修改可能没有什么实际意义,此处仅为说明问题使用。
在使用defer的时候可能出现类似问题,需要注意:
1 | for j := 0; j < 2; j++ { |
实例3:
1 | func incr() func() int { |
调用这个函数会返回一个函数变量。i := incr()
: 通过把这个函数变量赋值给i
, i
就成为了一个闭包
所以i
保存着对x
的引用, 可以想象i
中有着一个指针指向x
或者 i
中有x
的地址
由于i
有着指向x
的指针, 所以可以修改x
, 且保持着状态:
1 | println(i()) // 1 |
也就是说, x
逃逸了, 它的声明周期没有随着它的作用域结束而结束
但是这段代码却不会递增:
1 | println(incr()()) // 1 |
这是因为这里调用了三次 incr()
,返回了三个闭包,这三个闭包引用着三个不同的 x
,它们的状态是各自独立的。
实例4: 闭包引用产生的问题
1 | x := 1 |
因为闭包对外层词法域变量是引用的,所以这段代码会输出 3。
可以想象 f 中保存着 x 的地址,它使用 x 时会直接解引用,所以 x 的值改变了会导致 f 解引用得到的值也会改变。
但是,这段代码却会输出 1:
1 | x := 1 |
这是因为 f 调用时就已经解引用取值了,这之后的修改就与它无关了。
不过如果再次调用 f 还是会输出 3,这也再一次证明了 f 中保存着 x 的地址。
可以通过在闭包内外打印所引用变量的地址来证明:
1 | x := 1 |
可以看到引用的是同一个地址。
实例5.1: 循环闭包引用
1 | for i := 0; i < 3; i++ { |
这段代码相当于:
1 | for i := 0; i < 3; i++ { |
每次迭代后都对 i 进行了解引用并使用得到的值且不再使用,所以这段代码会正常输出。
实例5.2
正常代码:输出 0, 1, 2:
1 | var dummy [3]int |
然而这段代码会输出 3:
1 | var dummy [3]int |
实例5.3
1 | var funcSlice []func() |
为了解决上面这种情况, 可以声明新的匿名函数并传参:
1 | var funcSlice []func() |
现在 println(k)
使用的 k
是通过函数参数传递进来的,并且 Go 语言的函数参数是按值传递的。(把k
换成i
也没有问题, 即使它与for条件的中的i
和func的入参i
重名也能正常运行)
所以相当于在这个新的匿名函数内声明了三个变量,被三个闭包函数独立引用。原理跟第一种方法是一样的。
这里的解决方法可以用在大多数跟闭包引用有关的问题上,不局限于第三个例子。