匿名函数
函数字面量就是没有名字的函数。它是一个表达式,所以可以出现在任何能放值的地方:变量、参数、返回值、结构体字段。
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 作为变通办法;它没有害处,但已经不再需要。