Menu

Golang 闭包和匿名函数(附示例)

Go 中的匿名函数可以捕获外层作用域中的变量,由此形成闭包:计数器、生成器、中间件,以及保留自身状态的回调函数。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

匿名函数

函数字面量就是没有名字的函数。它是一个表达式,所以可以出现在任何能放值的地方:变量、参数、返回值、结构体字段。

Go 没有箭头函数或 lambda 简写。完整的 func(params) results { body } 形式是唯一的写法。

捕获变量

函数字面量可以使用外层函数中的变量。它不会复制这些变量:闭包和外围代码共享同一个变量。

只要还有闭包引用着被捕获的变量,它就会一直存活,即使声明它的函数已经返回。需要时编译器会把它移到堆上;这一切都不需要你自己管理。

生成器和计数器

从函数中返回一个闭包,每个返回的函数都会拥有自己私有的状态。

每次调用 counter() 都会创建一个新的 n,所以 c1 和 c2 各自独立计数。外部无法读取或重置 n,这算是一种小型的封装。

循环中的闭包(Go 1.22 及以后)

在 Go 1.22 之前,for 循环在整个循环中只有一个循环变量。在循环内创建的闭包捕获的都是这一个变量,看到的都是它的最终值。这是 Go 中最常见的闭包 bug。

从 Go 1.22 开始,每次迭代都有自己的副本,所以下面的代码会打印 0 1 2:

在 Go 1.21 及更早版本中,同样的循环写成 for i := 0; i < 3; i++(对整数使用 range 也是 1.22 新增的)会打印 3 3 3。新规则按模块生效,取决于 go.mod 中的 go 那一行,所以旧模块在把 go 行提升到 1.22 或更高之前,仍然保持旧行为。你在旧代码的循环中仍会看到 i := i 或 v := v:这是当年的手动修复,现在已经多余。

每次迭代一个新变量的规则只适用于 for 语句本身声明的变量。在循环之前声明、在循环体中更新的变量仍然是同一个共享变量:

var last string
for _, s := range items {
	last = s
	handlers = append(handlers, func() { use(last) }) // every closure sees the final value
}

闭包与 goroutine

用函数字面量启动的 goroutine 也是闭包。同样的共享规则依然适用,另外还要遵守并发的常规规则:如果多个 goroutine 写同一个被捕获的变量,就需要互斥锁或通道。

这段代码总是打印 5050。每个 goroutine 读取自己的 i(Go 1.22 的循环语义),并在加锁的情况下写入共享的 total。去掉互斥锁,结果就会变得无法预测;go run -race 会把它报告为数据竞争。WaitGroup 和竞态检测器的用法见 goroutine。

闭包在实际代码中的用处

  • 排序和查找:slices.SortFunc、slices.IndexFunc、sort.Slice 都接受函数字面量。
  • 延迟清理:defer func() { ... }() 在函数退出时运行一段代码,并且可以读取函数中的变量。见 defer。
  • HTTP 中间件:一个接收处理器并返回新的 http.HandlerFunc 字面量来包装它的函数。
  • 配置:函数式选项(func WithTimeout(d time.Duration) Option { return func(c *Config) { c.Timeout = d } })就是捕获了参数的闭包。

一个同时捕获参数和计数器的中间件示例:

常见错误

  • 以为拿到的是快照。 闭包在运行时读取变量的当前值,而不是创建闭包时的值。如果需要快照,请把值作为参数传入:go func(v int) { ... }(x)。
  • 递归的函数字面量。 在同一个 := 中,函数字面量不能通过它正被赋值的变量引用自己。请先声明变量:var walk func(n int); walk = func(n int) { ... walk(n-1) }。
  • 在 goroutine 中进行未同步的写入。 捕获不等于同步。请保护共享的写操作。

常见问题

Go 中的匿名函数是什么?

没有名字的函数字面量:func(x int) int { return x * 2 }。你可以把它赋值给变量、作为参数传递、作为返回值返回,或者在右花括号后加上 () 立即调用它。Go 没有单独的 lambda 语法;函数字面量就是 lambda。

Go 中的闭包是什么?

引用了外部声明的变量的函数字面量。函数会让这些变量一直存活,并与外层作用域共享它们,所以在闭包内部做的修改在外部可见,反过来也一样。变量是按引用捕获的,不会被复制。

Go 闭包是按值还是按引用捕获变量?

按引用。闭包和外围代码使用的是同一个变量。如果你需要一个快照,在创建闭包之前把值复制到一个新变量中,或者把它作为参数传入。

Go 修复了闭包循环变量的 bug 吗?

修复了,从 Go 1.22 开始。现在 for 循环的每次迭代都会声明一个新的循环变量,所以在循环中创建的闭包和 goroutine 看到的都是本次迭代的值。这适用于 go.mod 中写着 go 1.22 或更高版本的模块。旧代码常在循环中写 i := i 作为变通办法;它没有害处,但已经不再需要。

Coddy 编程语言插图

和 Coddy 一起学编程

开始