除没有名字外,匿名函数和普通函数完全相同。最大区别是,我们可在函数内部定义匿名函数,形成类似嵌套效果。匿名函数可直接调用,保存到变量,作为参数或返回值。
将匿名函数赋值给变量,与为普通函数提供名字标识符有着根本的区别。当然,编译器会为匿名函数生成一个“随机”符号名。
不曾使用的匿名函数会被编译器当作错误。
除闭包因素外,匿名函数也是一种常见重构手段。可将大函数分解成多个相对独立的匿名函数块,然后用相对简洁的调用完成逻辑流程,以实现框架和细节分离。
相比语句块,匿名函数的作用域被隔离(不使用闭包),不会引发外部污染,更加灵活。没有定义顺序限制,必要时可抽离,便于实现干净、清晰的代码层次。
闭包
闭包(closure)是在其词法上下文中引用了自由变量的函数,或者说是函数和其引用的环境的组合体。
闭包是如何实现的?匿名函数被返回后,为何还能读取环境变量值?
func test(x int) func() {
println(&x)
return func() {
println(&x, x)
}
}
func main() {
f := test(0x100)
f()
}
就这段代码而言,test 返回的匿名函数会引用上下文环境变量 x。当该函数在 main 中执行时,它依然可正确读取 x 的值,这种现象就称作闭包。
输出:
0xc82000a100
0xc82000a100 256
通过输出指针,我们注意到闭包直接引用了原环境变量。分析汇编代码,你会看到返回的不仅仅是匿名函数,还包括所引用的环境变量指针。所以说,闭包是函数和引用环境的组合体更加确切。
本质上返回的是一个 funcval 结构,可在 runtime/runtime2.go 中找到相关定义。
正因为闭包通过指针引用环境变量,那么可能会导致其生命周期延长,甚至被分配到堆内存。另外,还有所谓“延迟求值”的特性。
func test() []func() {
var s []func()
for i := 0; i < 2; i++ {
s = append(s, func() { // 将多个匿名函数添加到列表
println(&i, i)
})
}
return s // 返回匿名函数列表
}
func main() {
for _, f := range test() { // 迭代执行所有匿名函数
f()
}
}
输出:
0xc82000a078 2
0xc82000a078 2
对这个输出结果不必惊讶。很简单,for 循环复用局部变量 i,那么每次添加的匿名函数引用的自然是同一变量。添加操作仅仅是将匿名函数放入列表,并未执行。因此,当 main 执行这些函数时,它们读取的是环境变量 i 最后一次循环时的值。不是2,还能是什么?
解决方法就是每次用不同的环境变量或传参复制,让各自闭包环境各不相同。
同 js 中的 var(前端应该很好理解)
多个匿名函数引用同一环境变量,也会让事情变得更加复杂。任何的修改行为都会影响其他函数取值,在并发模式下可能需要做同步处理。
func test(x int) (func(), func()) { // 返回两个匿名函数
return func() {
println(x)
x += 10 // 修改环境变量
}, func() {
println(x) // 读取环境变量
}
}
func main() {
a, b := test(100)
a() // 100
b() // 110
}
闭包让我们不用传递参数就可读取或修改环境状态,当然也要为此付出额外代价。对于性能要求较高的场合,须慎重使用。