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