Go 闭包
Go 中闭包是捕获外部变量引用的函数值,常用于中间件、回调和并发控制,但需注意变量捕获的时机陷阱。
#type / concept
#status / growing
#tech / dev
#resource / go
[!info] related notes
- 所属 MOC: Go 语言基础 MOC
- 前置概念: Go 函数, Go 协程
- 并列概念: [[go-methods|Go 方法]], Go iota 与枚举
Go 闭包
一句话定义
闭包是一个函数值,它捕获并持有其定义作用域中外部变量的引用——即使外部函数已返回,闭包仍可通过该引用读写这些变量。
核心机制 / 工作原理
-
函数是第一类值:Go 中函数可以赋值给变量、作为参数传递、作为返回值返回。函数类型写作
func(参数) 返回值,如func(int) string。 -
闭包捕获变量引用,而非值:当一个内部函数引用了外部函数的变量,Go 编译器会将该变量”逃逸”到堆上,闭包持有对它的引用。多次调用外部函数产生的多个闭包共享(或各自持有)对应的变量实例。
-
goroutine 循环变量的经典陷阱:
for i := 0; i < 5; i++ { go func() { fmt.Println(i) // 所有 goroutine 可能都打印 5 }() }所有闭包捕获的是同一个
i的引用,循环结束后i的值是 5。Go 1.22 起修复了此问题——循环变量每次迭代创建新实例。但在 1.22 之前的版本或等价场景中,修复方式是:for i := 0; i < 5; i++ { i := i // 重新声明,创建新变量 go func() { fmt.Println(i) // 各自打印 0,1,2,3,4 }() } -
defer + 闭包的求值时机:
defer语句中的闭包在defer注册时捕获变量的引用(不是值),但闭包体在defer执行时(函数返回前)才运行。func f() int { x := 0 defer func() { x++ }() // 捕获 x 的引用 return x // 返回 0(返回值先被赋值),但 x 被 defer 改为 1(不影响返回值) } -
函数类型:
func(T) R是一个类型。可以声明变量、用作结构体字段、定义类型别名。零值是nil。
最小例子 / 最小场景
package main
import "fmt"
// 计数器工厂 — 经典闭包用法
func counter(start int) func() int {
n := start
return func() int {
n++ // 捕获外部的 n
return n
}
}
// 中间件模式
func withLogging(fn func(string)) func(string) {
return func(msg string) {
fmt.Println("[LOG] calling with:", msg)
fn(msg)
fmt.Println("[LOG] done")
}
}
func main() {
// 闭包保持状态
c1 := counter(0)
c2 := counter(100)
fmt.Println(c1(), c1(), c1()) // 1 2 3
fmt.Println(c2(), c2()) // 101 102
// 作为中间件
greet := func(name string) {
fmt.Println("Hello,", name)
}
loggedGreet := withLogging(greet)
loggedGreet("World")
// [LOG] calling with: World
// Hello, World
// [LOG] done
}
为什么重要
- HTTP 中间件的标准模式:Go web 框架(标准库
http.Handler、chi、echo 等)的中间件本质就是接收Handler返回Handler的闭包链。 - 并发控制:闭包常与 goroutine 配合,封装工作单元。
sync.WaitGroup、errgroup、context传参都依赖闭包捕获。 - 函数式编程模式:
map/filter/reduce、装饰器、策略模式——闭包使得这些模式在 Go 中自然实现。 - 资源封装:
defer中使用闭包可以精确控制清理逻辑中需要访问的变量。
边界与易混淆点
- 捕获的是引用,不是快照:闭包看到的始终是变量的最新值。如果需要快照,必须在创建闭包时将值拷贝到新变量(
v := v惯用法)。 - Go 1.22 循环变量语义变更:1.22 起
for循环变量每次迭代是新实例,go func(){ use(i) }()不再有竞态问题。但for range的v和for i := ...的行为差异仍需理解。 - 闭包逃逸到堆上:闭包引用的变量如果原本在栈上,会被编译器提升到堆上(逃逸分析)。小函数中的简单变量被闭包捕获会增加堆分配。
- nil 函数值调用会 panic:
var fn func(); fn()会 panic,必须确保函数值已初始化。 - 闭包不利于序列化:函数值不能 JSON 序列化、不能跨进程传递。如果需要可传输的回调,用函数注册表(map[string]func)替代。