《Go语言圣经》闭包

发布于:2025-06-21 ⋅ 阅读:(14) ⋅ 点赞:(0)
  • 闭包(Closure)是编程语言中一个强大的特性,它允许函数访问并记住其词法作用域内的变量,即使该函数在其原始作用域之外执行。简单来说,闭包是一个函数 + 该函数捕获的外部变量的引用
  • 闭包的核心特性捕获外部变量:闭包可以访问和修改其定义时所在作用域的变量,即使该作用域已经执行完毕
  • 变量状态保持:闭包会记住它捕获的变量,这些变量不会因为其原始作用域的结束而消失。
  • 独立实例:每次创建闭包时,都会生成一个独立的变量环境副本。

例子

闭包与变量捕获

首先看squares函数的定义:

func squares() func() int {
    var x int
    return func() int {
        x++
        return x * x
    }
}

这里发生了几件重要的事情:

  1. squares函数内部声明了一个局部变量x,类型为int。在Go中,未显式初始化的变量会被赋予零值,对于int类型来说就是0

  2. squares函数返回了一个匿名函数,这个匿名函数捕获了变量x。也就是说,这个匿名函数在其生命周期内会保留对x的引用。

  3. squares函数被调用时,它返回的匿名函数会带着对x的引用一起被返回。此时,squares函数的执行已经结束,通常其局部变量的生命周期也应该结束,但由于闭包的存在,x的生命周期被延长了。

变量x的存储位置

变量x并不存储在栈上(通常函数的局部变量存储在栈上),而是存储在上。这是Go语言编译器进行逃逸分析(Escape Analysis)后的结果。当编译器发现一个变量的生命周期可能超过其所在函数的执行周期时,它会将该变量分配到堆上,而不是栈上。

在这个例子中,x的生命周期通过闭包被延长了,因此它被分配到堆上。每次调用squares()都会创建一个新的闭包实例,每个实例都有自己独立的x变量。

初始值与状态保持

当你调用squares()并将结果赋值给f时:

f := squares()

你得到了一个闭包实例,它包含了自己的x变量,初始值为0。每次调用f()时,这个x的值都会递增并计算平方:

fmt.Println(f()) // x=1, 返回1*1=1
fmt.Println(f()) // x=2, 返回2*2=4
fmt.Println(f()) // x=3, 返回3*3=9
fmt.Println(f()) // x=4, 返回4*4=16

如果再次调用squares(),会创建一个新的闭包实例,拥有自己独立的x,初始值仍然是0:

g := squares()
fmt.Println(g()) // 又是1

总结

  1. 变量位置var x int存储在堆上,因为闭包延长了它的生命周期。
  2. 初始值:Go语言中未显式初始化的变量会被赋予零值,int的零值是0
  3. 状态保持:每次调用squares()都会创建一个新的闭包实例,每个实例都有自己独立的x,其状态在多次调用之间保持。

这种闭包特性在需要维护状态的场景中非常有用,比如生成器、计数器等。

闭包可能的问题

问题核心:闭包捕获的是变量本身,而非变量的值

先看一个错误示例:

var funcs []func()
for i := 0; i < 3; i++ {
    funcs = append(funcs, func() {
        fmt.Print(i, " ") // 错误:所有闭包共享同一个i变量
    })
}

for _, f := range funcs {
    f() // 输出:3 3 3(而不是0 1 2)
}

为什么输出3 3 3?

  • 闭包捕获的是变量i的内存地址,而不是循环迭代时的瞬时值。
  • 当循环结束时,i的值已经变为3,此时所有闭包再执行,读取的都是这个最终值。

正确做法:为每个闭包创建独立的变量副本

var funcs []func()
for i := 0; i < 3; i++ {
    j := i // 关键:创建副本,每个闭包捕获自己的j
    funcs = append(funcs, func() {
        fmt.Print(j, " ") // 正确:输出0 1 2
    })
}

for _, f := range funcs {
    f() // 输出:0 1 2
}

关键点

  • 在每次循环中,j := i创建了一个新的局部变量j,并初始化为当前i的值。
  • 每个闭包捕获的是不同的j变量(内存地址不同),因此它们的值相互独立。

再看一个字符串的例子

var greetings []func()
names := []string{"Alice", "Bob", "Charlie"}

for _, name := range names {
    // 错误写法:所有闭包共享同一个name变量
    greetings = append(greetings, func() {
        fmt.Println("Hello,", name)
    })
}

for _, greet := range greetings {
    greet() // 输出:Hello, Charlie(重复3次)
}

修正后

for _, name := range names {
    name := name // 创建副本
    greetings = append(greetings, func() {
        fmt.Println("Hello,", name) // 正确输出:Alice, Bob, Charlie
    })
}

总结

闭包捕获变量的规则:

  1. 闭包捕获的是变量本身(内存地址),而非变量的值。
  2. 如果循环内部的闭包引用了循环变量,所有闭包将共享同一个变量。
  3. 为了让每个闭包拥有独立的状态,需要在循环内部创建副本变量。

为什么Go语言要这样设计?

这是语言设计的权衡:

  • Go希望闭包捕获变量的方式简单一致(总是捕获变量本身)。
  • 开发者需要主动管理变量作用域,避免意外共享状态。

类似的问题在其他语言中也存在(如JavaScript),但解决方式可能不同(如JavaScript的let关键字会为每次循环迭代创建独立变量)。


网站公告

今日签到

点亮在社区的每一天
去签到