go匿名函数和闭包

函数变量(函数值)

在 Go 语言中,函数被看作是第一类值,这意味着函数像变量一样,有类型、有值,其他普通变量能做的事它也可以。

1
2
3
func square(x int) {
println(x * x)
}
  1. 直接调用:square(1)
  2. 把函数当成变量一样赋值:s := square;接着可以调用这个函数变量:s(1)。 注意:这里 square 后面没有圆括号,调用才有。
  • 调用 nil 的函数变量会导致 panic。
  • 函数变量的零值是 nil,这意味着它可以跟 nil 比较,但两个函数变量之间不能比较。

匿名函数

作用: 在go语言中目前了解的作用就是用于构成闭包

闭包

闭包通过引用的方式使用外部函数的变量
函数与 与其(直接)相关的环境形成闭包

简单来说: 因为把返回的函数赋给了一个变量, 虽然函数在执行完一瞬间会销毁其执行环境, 但是如果有闭包的话, 闭包会保存外部函数的活动对象(变量), 所以如果不对闭包的引用消除掉, 闭包会一直存在内存中, 垃圾收集器不会销毁闭包占用的内存

实例1

1
2
3
4
5
6
7
8
9
10
//函数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:

1
2
3
4
5
6
7
8
9
10
func main() {
a := A()//定义变量a,并将函数A的返回值赋给a // 这个时候, 虽然有小括号, 但是func A()还未真正执行, 只是赋值给了变量a
b := a(4) //真正执行func A()
fmt.Println(b)
}
/*
** 输出:
** bb= 4 sum= 4
** 4
*/

调用2

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
a := A()
a(0)
a(1)
a(5)
}
/*
**  输出:
**  bb= 0 sum= 0
**  bb= 1 sum= 1
**  bb= 5 sum= 6
*/

以上调用通过闭包实现了sum的累加

调用3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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。
  
可作如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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的时候可能出现类似问题,需要注意:

1
2
3
4
5
6
7
8
9
10
for j := 0; j < 2; j++ {
defer (func() {
fmt.Println(j)
})()
}
/*
** 输出:
** 2
** 2
*/

实例3:

1
2
3
4
5
6
7
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 , 且保持着状态:

1
2
3
println(i()) // 1
println(i()) // 2
println(i()) // 3

也就是说, x 逃逸了, 它的声明周期没有随着它的作用域结束而结束
但是这段代码却不会递增:

1
2
3
println(incr()()) // 1
println(incr()()) // 1
println(incr()()) // 1

这是因为这里调用了三次 incr(),返回了三个闭包,这三个闭包引用着三个不同的 x,它们的状态是各自独立的。

实例4: 闭包引用产生的问题

1
2
3
4
5
6
7
x := 1
f := func() {
println(x)
}
x = 2
x = 3
f() // 3

因为闭包对外层词法域变量是引用的,所以这段代码会输出 3。
可以想象 f 中保存着 x 的地址,它使用 x 时会直接解引用,所以 x 的值改变了会导致 f 解引用得到的值也会改变。
但是,这段代码却会输出 1:

1
2
3
4
5
6
x := 1
func() {
println(x) // 1
}()
x = 2
x = 3

这是因为 f 调用时就已经解引用取值了,这之后的修改就与它无关了。

不过如果再次调用 f 还是会输出 3,这也再一次证明了 f 中保存着 x 的地址。
可以通过在闭包内外打印所引用变量的地址来证明:

1
2
3
4
5
x := 1
func() {
println(&x) // 0xc0000de790
}()
println(&x) // 0xc0000de790

可以看到引用的是同一个地址。

实例5.1: 循环闭包引用

1
2
3
4
5
for i := 0; i < 3; i++ {
func() {
println(i) // 0, 1, 2
}()
}

这段代码相当于:

1
2
3
4
5
6
for i := 0; i < 3; i++ {
f := func() {
println(i) // 0, 1, 2
}
f()
}

每次迭代后都对 i 进行了解引用并使用得到的值且不再使用,所以这段代码会正常输出。

实例5.2

正常代码:输出 0, 1, 2:

1
2
3
4
var dummy [3]int
for i := 0; i < len(dummy); i++ {
println(i) // 0, 1, 2
}

然而这段代码会输出 3:

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
8
9
10
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
}

为了解决上面这种情况, 可以声明新的匿名函数并传参:

1
2
3
4
5
6
7
8
9
10
11
12
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 重名也能正常运行)

所以相当于在这个新的匿名函数内声明了三个变量,被三个闭包函数独立引用。原理跟第一种方法是一样的。

这里的解决方法可以用在大多数跟闭包引用有关的问题上,不局限于第三个例子。