go语言闭包和for循环

150 阅读2分钟

闭包

我们知道在GO语言中,函数是一等公民。它既可以作为参数,也可以作为函数的返回值返回。闭包通俗地说就是可以访问外部函数内部变量的函数。它通过存储相应的函数和捕获的变量来实现。

在讲闭包之前,我们先学习一下Go语言中的函数。Go 语言中Function value本质上是一个指针,但是其并不直接指向函数的入口地址,而是指向的runtime.funcval( runtime/runtime2.go)这个结构体。该结构体中的fn字段存储的是函数的入口地址。以下面这段代码为例:

func A(i int) {
	i++
	fmt.Println(i)
}

func B() {
	f1 := A
	f1(1)
}

func C() {
	f2 := A
	f2(2)
}

它在内存中的布局如下: image.png f1f2并不直接保存函数的地址。而是指向fnfn结构体中才保存着相应的函数指针。之所以通过二级指针来指向函数是因为闭包的设计。因为闭包是有状态的。它不仅需要保存相应的函数,还需要保存捕获的变量。 以下函数举例:

package main

func A() func() int {
    i := 3
    return func() int {
        return i
    }
}

func main() {
    f1 := A()
    f2 := A()
    
    print(f1())
    print(f2())
}

当main函数执行,它会在main函数的栈区分配f1、f2变量,它指向保存在堆区的funcval结构体,同时紧挨着该结构体就有该闭包函数捕获的变量。这也就解释了为什么要使用二级指针来保存了。

image.png 通过查看代码编译期间的变量逃逸情况,可以发现是符合我们的分析的:

image.png 标红的那一行代码就是闭包函数return那句。

需要注意的是,当捕获外部函数的内部变量且该变量会发生改变的时候,该变量就会逃逸到堆上。

package main

func app() func(string) string {
    t := "Hi"
    c := func(b string) string {
       t = t + " " + b
       return t
    }
    return c
}

func main() {
    a := app()
    b := app()
    a("go")
    b("All")
}

大家可以自行通过编译语句验证。

闭包和协程

协程的开启是异步的且需要一定时间。在go1.23之前,下面的代码都只会打印:3, 3, 3

package main

import (
    "fmt"
    "time"
)

func main() {
    for i := 0; i < 3; i++ {
       go func() {
          fmt.Println(i)
       }()
    }
    time.Sleep(1 * time.Second)
}

当协程开启的时候,这时候循环变量已经变成3了。我们可以通过传递参数或者对捕获变量进行拷贝来解决该问题。