本文已参与「新人创作礼」活动, 一起开启创作之路
引言
对golang有些了解的读者,都知道函数在golang中是一等公民,对于函数的一些基本定义和使用,在本文中就不在赘述,我们主要介绍下匿名函数和闭包,这两个概念在很多的框架底层源码中还是非常常见的。
匿名函数
顾名思义,匿名函数就是我们没有函数名称的函数,匿名函数只包括 参数列表、返回值列表:
func(参数列表)(返回参数列表){
函数体
}
举个栗子:
func main() {
sq:=func (f float64) float64{
return math.Sqrt(f)
}
fmt.Println(sq(4))
}
匿名函数还可以在声明后直接调用:
func main() {
sq:=func (f float64) float64{
return math.Sqrt(f)
}(4)
fmt.Println(sq)
}
匿名函数做回调函数
匿名函数做回调函数在go语言的设计中非常的常见:
func main() {
var arrs = []int{1,2,3}
lists(arrs, func(i int) {
fmt.Printf("i:=%+vn",i )
})
}
func lists(arr []int, f func(int)) {
for _, i := range arr {
f(i)
}
}
上面几个小例子应该已经很清楚的为我们介绍了匿名函数的一些基本用法, 匿名函数是闭包的一个基础,我们对匿名函数有了一定的了解以后,下面我们开始介绍闭包。
闭包
基本定义
所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。闭包=函数+引用环境。
上面的介绍看着非常的专业,但是又好像啥也没有介绍,因为完全没有看懂,所以呢,如果想更加清楚的介绍闭包,我们还是上例子:
func incr() func() int {
var x int
return func() int {
x++
return x
}
}
调用这个函数会返回一个函数变量。
in:=incr()
通过把这个函数变量赋值给in,in变量就成了一个闭包。
所以,in中就保存着对x的引用,可以想象成 in中有着一个指针指向x或者说in中有x的地址
由于in中有着指向x的指针,所以可以修改x,并且可以保持状态。
func main() {
in:=incr()
println(in()) // 1
println(in()) // 2
println(in()) // 3
}
也就说,x变量逃逸了,他生命周期没有随着它的作用域结束而结束。
通过对上面这个小例子的分析,我们现在在反过来琢磨一下,官方对闭包的定义:闭包=函数+引用环境。
接下来,我们看下这段代码:
println(incr()()) // 1
println(incr()()) // 1
println(incr()()) // 1
这个代码返回值都是1,没有进行递增,这是因为我们这里调用了三次incr(),返回了三个闭包,这三个闭包持有了三个不同的对x的引用,他们的状态是各自独立的。
闭包的引用
x := 1
f := func() {
println(x)
}
x = 2
x = 3
f() // 3
这段代码输出3,如果对上面的分析理解到位了,这里应该比较好理解。因为,f中保存了x的引用,它使用的时候直接解引用,所以x的值随着程序的执行而改变,最后的值为3,所以f中对x的解引用的结果也是3.
x := 1
func() {
println(x) // 1
}()
x = 2
x = 3
上面输出结果1,因为在调用f的时候就进行解引用了,后面的值修改对f没有影响了。其实上面这段代码就等价于下面代码:
x := 1
f := func() {
println(x)
}
f() // 1
x = 2
x = 3
循环闭包引用
func main() {
s := []string{"a", "b", "c"}
for _, v := range s {
go func() {
fmt.Println(v)
}()
}
time.Sleep(time.Second * 1)
}
结果会是什么呢? a, b, c? 错了,结果是 c, c, c。为什么呢?
这是因为for语句里面中闭包使用的v是外部的v变量,当执行完循环之后,v最终是c,所以输出了 c, c, c。 如果你去执行,有可能也不是这个结果。 输出这个结果的前提是“在主协程执行完for之后,定义的子协程 才开始执行,如果for过程中,子协程执行了,结果就可能不是c, c,c”。 输出的结果依赖于子协程执行时的那一刻,v是什么。
如果我们想输入a b c 怎么做呢?
func main() {
s := []string{"a", "b", "c"}
for _, v := range s {
go func() {
fmt.Println(v)
}()
time.Sleep(time.Second * 3)
}
fmt.Println("main routine")
time.Sleep(time.Second * 1) // 阻塞模式
}
此时输出的就是 a, b, c , main routine
为什么这次有正常了呢? 这是因为在for循环中执行了sleep, 让每次for循环中新定义的子协程有时间执行,子协程执行时获取环境中的变量v, 那么每次就会是本次循环执行时变量v的实际值。
另外一种方法,也是我们最为常用的一种,只需要每次将变量v的拷贝传进函数即可,但此时就不是使用的上下文环境中的变量了。
func main() {
s := []string{"a", "b", "c"}
for _, v := range s {
go func(v string) {
fmt.Println(v)
}(v) //每次将变量 v 的拷贝传进函数
}
select {}
}
延迟调用与闭包
defer 调用会在当前函数执行结束前才被执行,这些调用被称为延迟调用 。defer 中使用匿名函数依然是一个闭包。
package main
import "fmt"
func main() {
x, y := 1, 2
defer func(a int) {
fmt.Printf("x:%d,y:%dn", a, y) // y 为闭包引用
}(x) // 复制 x 的值
x += 100
y += 100
fmt.Println(x, y)
}
输出结果:
101 102
x:1,y:102
为什么在defer中的x是1而不是101呢?
其实是原因是 在defer定义时 已经将x的拷贝 1 复制给了defer, defer执行时使用的是当时defer定义时x的拷贝,而不是当前环境中x的值。
为什么要用闭包?
匿名自执行函数:我们知道所有的变量,如果不加上 var 关键字,则默认的会添加到全局对象的属性上去,这样的临时变量加入全局对象有很多坏处,比如:别的函数可能误用这些变量;造成全局对象过于庞大,影响访问速度(因为变量的取值是需要从原型链上遍历的)。除了每次使用变量都是用 var 关键字外,我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,可以用闭包。
结果缓存:我们开发中会碰到很多情况,设想我们有一个处理过程很耗时的函数对象,每次调用都会花费很长时间,那么我们就需要将计算出来的值存储起来,当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,然后更新缓存并返回值,如果找到了,直接返回查找到的值即可。闭包正是可以做到这一点,因为它不会释放外部的引用,从而函数内部的值可以得以保留。
总结
本文主要是介绍了匿名函数和闭包,我们在平常使用的是有的一些诡异的问题,尤其是我们如果不注意会引起一些逻辑bug,如果对这些概念和使用不是很熟悉,排查起来也比较费时间,希望通过本文给您带来一些帮助。