插件窝 干货文章 Go语言闭包中,为什么循环内匿名函数总是输出循环变量的最终值?

Go语言闭包中,为什么循环内匿名函数总是输出循环变量的最终值?

变量 闭包 循环 捕获 573    来源:    2025-03-11

在Go语言中,闭包(closure)是指一个函数值(function value)捕获了其外部作用域中的变量。当你在循环中使用匿名函数(闭包)时,如果这个闭包捕获了循环变量,那么所有闭包实例都会共享同一个循环变量。由于闭包是延迟执行的(例如在循环结束后才执行),它们会捕获循环变量的最终值,而不是每次迭代时的值。

示例代码

package main

import (
    "fmt"
    "time"
)

func main() {
    for i := 0; i < 3; i++ {
        go func() {
            fmt.Println(i)
        }()
    }
    time.Sleep(time.Second) // 等待goroutine执行完毕
}

输出结果

3
3
3

原因分析

  1. 共享变量:在上面的代码中,匿名函数捕获了循环变量 i。由于 i 是循环变量,它在每次迭代时都会被更新。所有的匿名函数都共享同一个 i 变量。

  2. 延迟执行go 关键字启动了新的 goroutine,这些 goroutine 是并发执行的。由于 goroutine 的调度是异步的,循环可能会在 goroutine 开始执行之前就已经完成。因此,当 goroutine 开始执行时,i 的值已经是循环结束后的最终值(在这个例子中是 3)。

解决方法

为了避免这个问题,你可以在每次迭代时创建一个新的局部变量,并将循环变量的值赋给这个局部变量。这样每个闭包都会捕获不同的变量实例。

修改后的代码

package main

import (
    "fmt"
    "time"
)

func main() {
    for i := 0; i < 3; i++ {
        i := i // 创建一个新的局部变量
        go func() {
            fmt.Println(i)
        }()
    }
    time.Sleep(time.Second) // 等待goroutine执行完毕
}

输出结果

0
1
2

解释

  • i := i 创建了一个新的局部变量 i,它的作用域仅限于当前迭代。每个 goroutine 捕获的是不同的 i 变量,因此输出的值是正确的。

另一种解决方法

你也可以将循环变量作为参数传递给匿名函数:

package main

import (
    "fmt"
    "time"
)

func main() {
    for i := 0; i < 3; i++ {
        go func(i int) {
            fmt.Println(i)
        }(i)
    }
    time.Sleep(time.Second) // 等待goroutine执行完毕
}

输出结果

0
1
2

解释

  • 在这个例子中,i 作为参数传递给匿名函数,每个 goroutine 都会接收到一个独立的 i 值,因此输出的值是正确的。

总结

在Go语言中,闭包捕获的是变量的引用,而不是变量的值。因此,在循环中使用闭包时,需要注意循环变量的作用域和生命周期。通过创建局部变量或将循环变量作为参数传递给闭包,可以避免闭包捕获到循环变量的最终值。