七叶笔记 » golang编程 » Golang匿名函数与闭包

Golang匿名函数与闭包

一、匿名函数

浅显理解: 说到匿名函数,从字面理解就是 没有名字的函数

深入原理: 其在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语言本身来决定的,我们只需要知道这个原则,就能知道闭包里的函数应该是个什么值。

相关文章