go--函数调用--闭包

闭包定义

闭包是由函数和与其相关的引用环境组合而成的实体(即: 闭包=函数+引用环境)

闭包知识点

Go语言支持闭包
Go语言能通过escape analyze识别出变量的作用域,自动将变量在堆上分配。将闭包环境变量在堆上分配是Go实现闭包的基础。
返回闭包时并不是单纯返回一个函数,而是返回了一个结构体,记录下函数返回地址和引用的环境中的变量地址。

闭包结构体

闭包结构体
回到闭包的实现来,前面说过,闭包是函数和它所引用的环境。那么是不是可以表示为一个结构体呢:

type Closure struct {
    F func()() 
    i *int
}

事实上,Go在底层确实就是这样表示一个闭包的。让我们看一下汇编代码:

func f(i int) func() int {
    return func() int {
        i++
        return i
    }
}
MOVQ    $type.int+0(SB),(SP)
PCDATA    $0,$16
PCDATA    $1,$0
CALL    ,runtime.new(SB)    // 是不是很熟悉,这一段就是i = new(int)    
...    
MOVQ    $type.struct { F uintptr; A0 *int }+0(SB),(SP)    // 这个结构体就是闭包的类型
...
CALL    ,runtime.new(SB)    // 接下来相当于 new(Closure)
PCDATA    $0,$-1
MOVQ    8(SP),AX
NOP    ,
MOVQ    $"".func·001+0(SB),BP
MOVQ    BP,(AX)                // 函数地址赋值给Closure的F部分
NOP    ,
MOVQ    "".&i+16(SP),BP        // 将堆中new的变量i的地址赋值给Closure的值部分
MOVQ    BP,8(AX)
MOVQ    AX,"".~r1+40(FP)
ADDQ    $24,SP
RET    ,

其中func·001是另一个函数的函数地址,也就是f返回的那个函数。

考点

考点1:闭包引用环境

func f(i int) func() int {
    return func() int {
        i++
        return i
    }
}

函数f返回了一个函数,返回的这个函数,返回的这个函数就是一个闭包。这个函数中本身是没有定义变量i的,而是引用了它所在的环境(函数f)中的变量i。
c1 := f(0)
c2 := f(0)
c1() // reference to i, i = 0, return 1
c2() // reference to another i, i = 0, return 1
c1跟c2引用的是不同的环境,在调用i++时修改的不是同一个i,因此两次的输出都是1。函数f每进入一次,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境。

考点2: 闭包在循环语句中的引用环境

  • 例1
for i := 0; i < 3; i++ {
    func() {
        println(i) 
    }()
}

解答:
这段代码相当于

for i := 0; i < 3; i++ {
    f := func() {
        println(i) 
    }
    f()
}

这样就很清楚的能看出来最后的输出为 0,1,2

  • 例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

把循环转换成这样的形式就容易理解了:

var dummy [3]int
var f func()
for i := 0; i < len(dummy); {
    f = func() {
        println(i)
    }
    i++
}
f() // 3

复制代码i 自加到 3 才会跳出循环,所以循环结束后 i 最后的值为 3
所以用 for range 来实现这个例子就不会这样:

var dummy [3]int
var f func()
for i := range dummy {
    f = func() {
        println(i)
    }
}
f() // 2

复制代码这是因为 for range 和 for 底层实现上的不同。

考点3: 闭包列表

  • 例1
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
}

复制代码输出序列为 3, 3, 3。
看了前面的例子之后这里就容易理解了:
这三个函数引用的都是同一个变量(i)的地址,所以之后 i 递增,解引用得到的值也会递增,所以这三个函数都会输出 3。

  • 例2
var funcSlice []func()
for i := 0; i < 3; i++ {
    func(i int) {
        funcSlice = append(funcSlice, func() {
            println(i)
        })
    }(i)

}
for j := 0; j < 3; j++ {
    funcSlice[j]() // 0, 1, 2
}

现在 println(i) 使用的 i 是通过函数参数传递进来的,并且 Go 语言的函数参数是按值传递的。
所以相当于在这个新的匿名函数内声明了三个变量,被三个闭包函数独立引用。原理跟第一种方法是一样的。
这里的解决方法可以用在大多数跟闭包引用有关的问题上

参考博客

https://juejin.im/post/5c850d035188257ec629e73e#heading-5
https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.6.html

你可能感兴趣的:(go--函数调用--闭包)