编程高级 Go 开发者

请告诉我 Go 如何实现闭包(函数字面量/闭包),以及使用闭包时的限制和特性:它们存储在哪里,如何捕获变量,不同场景下捕获变量的行为有什么不同?

用 Hintsage AI 助手通过面试

答案

在 Go 中,匿名函数(函数字面量)能够创建闭包,即在其外部作用域完成后依然可以访问变量。这样的闭包如果需要正确运行,会在堆中分配内存(通过逃逸分析检测)。

示例:

func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } a := adder() printf("%d ", a(5)) // 5 printf("%d ", a(10)) // 15

特性:

  • 闭包通过引用捕获外部作用域的变量(而不是在创建时的值)。
  • 如果变量在闭包外被更改,闭包将看到新值。
  • 如果闭包从函数返回,被捕获的变量的生命周期将持续到闭包的生命周期结束。
  • 如果闭包未被使用,逃逸分析可能允许变量不进入堆。

陷阱问题

这段代码会输出什么?

func main() { fs := []func(){} for i := 0; i < 3; i++ { fs = append(fs, func() { fmt.Println(i) }) } for _, f := range fs { f() } }

许多人会回答输出 0, 1, 2,但实际上结果是:

3
3
3

所有闭包都引用同一个变量 i;循环结束后它的值为 3。

正确做法:在循环体中捕获变量的副本:

for i := 0; i < 3; i++ { v := i // 新的变量 fs = append(fs, func() { fmt.Println(v) }) }

由于不懂该主题的细微差别而导致的实际错误示例


故事

在动态路由项目中,使用循环通过闭包创建了许多处理程序,每个处理程序都应该捕获自己的路径。结果所有处理程序打印了最后一个路径——没有在每个闭包中创建单独的变量。错误仅在与 HTTP API 集成时才被发现。


故事

在测试通过 goroutine 的并发访问时,闭包捕获了索引的引用,而不是副本。这导致了“随机”效果:数据没有写入自己的数组槽,而是写入了最后一个槽。


故事

在统计收集函数中,闭包修改了外部作用域的全局变量,尽管作者期望每个任务有独立的计数器。问题通过不恰当的重建总和被发现,总和总是公共的而不是私有的,尽管有局部逻辑。