go匿名函数和闭包

函数变量(函数值)

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

func square(x int) {
	println(x * x)
}
  1. 直接调用:square(1)
  2. 把函数当成变量一样赋值: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 重名也能正常运行)

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

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

Licensed under CC BY-NC-SA 4.0
Built with Hugo
主题 StackJimmy 设计