一、匿名函数
浅显理解: 说到匿名函数,从字面理解就是 没有名字的函数 。
深入原理: 其在Golang中得以实现的根据是Golang支持 函数字面量 (这里可以百度一下就清楚了,还有一个东西叫函数表达式可以参照对比学习)。
究其原理: Golang语言中, 函数是头等对象,可以当做参数传递,可以做函数返回值,也可以绑定到变量 。在Go语言称这样的参数、返回值或变量为Function Value。函数的指令在编译期间生成,而Function Value本质上是一个指针,但是这个指针并不直接指向函数指令入口,具体原因,下文说的闭包在详细解释。
匿名函数三种使用方式:
1、全局匿名函数
package main
import "fmt"
//全局匿名函数
var (
AnonymousFunc = func(a int, b int) int {
return a + b
}
)
func main() {
fmt.Println(AnonymousFunc(1, 2))
}
2、匿名函数直接调用,该函数只调用一次,最常见defer
package main
import "fmt"
func main() {
defer func() {
fmt.Println("defer anonymous function run")
}()
res := func(a, b int) int {
return a + b
}(1, 2)
fmt.Println("求和结果:", res)
}
3、当变量使用
package main
import "fmt"
func main() {
var f func(a, b int) int // 定义一个函数类型的变量f
f = func(a, b int) int { // 赋值
return a + b
}
fmt.Println("求和结果:", f(1, 2))
f2 := func(a, b int) int {
return a + b
}
fmt.Println("求和结果:", f2(1, 2))
}
二、闭包
常识性解释: 在Golang语言中有这个说法:匿名函数就是闭包。(为什么呢?)
官方解释: 闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者 任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块(由于自由变量包含 在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环 境(作用域)。是不是晦涩难懂,读了跟没读一样?其实就一句话:闭包是一个函数和与他绑定的外部环境的集合(捕获的外边变量列表)
到底什么是闭包: 这里有 三个要点 需要大家理解
要点1 、闭包在实现上是 一个结构体 ,它存储了 一个函数 (通常是其入口地址)和一个关联的环境(相当于一个符号查找表)
要点2、 要包括 自由变量 (在函数外部定义但在函数内被引用)
要点3、 即便脱离了捕捉时的 上下文 ,它也能照常运行(因为捕获了外部变量)
理解要点1: 在这里再回看上文在匿名函数中提到的Function Value结构,再此展开一下: Function Value本质是一个指针,指针指向一个runtime.funcval结构体,这个结构体里只有一个地址,这个地址指向函数的入口地址 ,那么为什么不让变量直接指向函数入口地址,而要用一个二级地址呢?就是为了实现闭包。
type funcval struct {
fn uintptr
// variable-size, fn-specific data here
}
理解要点2: 那么闭包函数已经有了,那要点中的自由变量呢,闭包函数捕获的变量列表怎么使用呢?Go语言中通过Function Value 调用函数时,会把对应的Function Value结构体地址,存入特定寄存器(例如arm的amd64平台使用的是DX寄存器),这样,在闭包函数中,就可以通过寄存器去查funcval结构体的地址,然后加上相应的偏移量,来找到每一个捕获的变量,所以在Go语言中,闭包就是有捕获列表的Function Value,而没有捕获列表的Function Value 直接忽略这个寄存器的值就可以了。归纳: 外层函数的变量都发生了内存拷贝,存在了捕获列表中,备用。
理解要点3: 这个捕获列表可不是拷贝变量值这么简单, 被闭包捕获的变量可以是值拷贝,也可以是名称引用 ,要使变量在外层函数与闭包函数中表现一致,好像他们在使用同一个变量。
示例:
package main
import (
"fmt"
"sync"
"time"
)
// AnonymousFunc1 普通闭包,变量i被捕获
func AnonymousFunc1() func() int {
var i int // 定义一个局部变量
i = 1
return func() int {
return i // 捕获的变量,内部与外部i变量表现一致
}
}
//AnonymousFunc2 捕获引用
func AnonymousFunc2() func() int {
var i int // 定义一个局部变量
f := func() int {
return i // 内部与外部变量表现一致,此处显然捕获的不是值而是引用
}
i = 2
return f
}
//AnonymousFunc3 捕获引用
func AnonymousFunc3() (fs [2]func() int) {
for i := 0; i < 2; i++ {
fs[i] = func() int {
return i // 内部与外部变量表现一致,此处捕获的是引用
}
}
return
}
//AnonymousFunc4 捕获的val的值
func AnonymousFunc4() (fs [2]func() int) {
for i := 0; i < 2; i++ {
val := i
fmt.Println("AnonymousFunc4 中 i与val参数地址:", &i, &val)
fs[i] = func() int {
return val // val每次的表现都是新定义,新地址,此处捕获的就是遍历val的值
}
}
return
}
//AnonymousFunc5 还有一个好玩的
func AnonymousFunc5() {
var flag int
var wg sync.WaitGroup
wg.Add(4)
for i := 0; i < 2; i++ {
go func(i int) { // 在此处有值拷贝
flag++
fmt.Println("AnonymousFunc5 的第", i, "次捕获的值")
wg.Done()
}(i)
}
for i := 0; i < 2; i++ {
go func() {
flag++
fmt.Println("AnonymousFunc5 的第", i, "次捕获的值")
wg.Done()
}()
}
time.Sleep(time.Duration(3) * time.Second) // 为了让程序执行完
fmt.Println("AnonymousFunc5 在闭包执行完应该在外层函数也体现flag的值,这个值应该是【4】,实际结果:", flag)
return
}
func main() {
f1 := AnonymousFunc1()
fmt.Println("AnonymousFunc1 的结果:", f1())
f2 := AnonymousFunc2()
fmt.Println("AnonymousFunc2 的结果:", f2())
f3 := AnonymousFunc3()
for i := 0; i < len(f3); i++ {
fmt.Println("AnonymousFunc3 的第", i, "结果:", f3[i]())
}
f4 := AnonymousFunc4()
for i := 0; i < len(f4); i++ {
fmt.Println("AnonymousFunc3 的第", i, "结果:", f4[i]())
}
AnonymousFunc5()
}
输出结果:
AnonymousFunc1 的结果: 1
AnonymousFunc2 的结果: 2
AnonymousFunc3 的第 0 结果: 2
AnonymousFunc3 的第 1 结果: 2
AnonymousFunc4 中 i与val参数地址: 0xc00009c050 0xc00009c058
AnonymousFunc4 中 i与val参数地址: 0xc00009c050 0xc00009c060
AnonymousFunc3 的第 0 结果: 0
AnonymousFunc3 的第 1 结果: 1
AnonymousFunc5 的第 0 次捕获的值
AnonymousFunc5 的第 2 次捕获的值
AnonymousFunc5 的第 1 次捕获的值
AnonymousFunc5 的第 2 次捕获的值
AnonymousFunc5 在闭包执行完应该在外层函数也体现flag的值,这个值应该是【4】,实际结果: 4
总结:
从结果中可以看出,捕获地址还是引用的原则就是:变量要在外层函数与闭包函数中表现一致,好像他们在使用同一个变量。这是go语言本身来决定的,我们只需要知道这个原则,就能知道闭包里的函数应该是个什么值。