上一篇我们学习了匿名函数 Anonymous Function,知道函数不仅可以拥有名字,也可以直接写成:
go
func() {
fmt.Println("Hello Go")
}
匿名函数还可以保存到变量、作为参数传递,甚至作为另一个函数的返回值。在学习匿名函数时,我们留下了一个非常重要的问题:
scss
count := 0
add := func() {
count++
}
add()
add()
fmt.Println(count)
最终输出:
2
为什么匿名函数内部可以访问并修改外部的 count?更有意思的是,一个函数已经执行结束后,它内部创建的某些变量为什么还能继续存在?这就涉及 Go 函数体系中非常重要的概念------闭包 Closure。
一、什么是闭包
闭包可以简单理解为:函数与它所引用的外部变量组合形成的整体 。
例如:
scss
func main() {
count := 0
add := func() {
count++
fmt.Println(count)
}
add()
add()
add()
}
输出:
1
2
3
匿名函数:
scss
func() {
count++
fmt.Println(count)
}
内部没有定义 count,却使用了外层作用域中的:
go
count := 0
此时匿名函数捕获了外部变量 count,形成闭包。
可以简单理解为:
闭包 = 函数 + 被函数引用的外部环境
二、最简单的闭包
例如:
go
package main
import "fmt"
func main() {
name := "Tom"
hello := func() {
fmt.Println("Hello", name)
}
hello()
}
输出:
Hello Tom
name 定义在匿名函数外部:
go
name := "Tom"
匿名函数内部直接使用:
arduino
fmt.Println("Hello", name)
因此匿名函数捕获了 name。
如果修改:
ini
name = "Jack"
hello()
输出:
Hello Jack
闭包引用的是变量本身,而不是简单复制变量定义那一刻的值。
三、闭包可以修改外部变量
例如:
scss
func main() {
count := 0
add := func() {
count++
}
add()
add()
add()
fmt.Println(count)
}
输出:
3
匿名函数不仅能读取:
count
还可以修改它。
这也是闭包最常见的用途之一:保存某种状态。
四、函数返回闭包
闭包真正有意思的地方,是函数可以返回一个引用内部变量的函数。
例如:
go
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
调用:
css
func main() {
c := counter()
fmt.Println(c())
fmt.Println(c())
fmt.Println(c())
}
输出:
1
2
3
这里:
scss
counter()
执行结束之后,按照普通局部变量的理解:
go
count := 0
似乎应该已经不能访问。
但返回的匿名函数仍然引用 count,因此这个变量仍然能够被后续调用使用。
这就是闭包最核心的特性之一。
五、闭包为什么能记住变量
看下面的代码:
go
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
第一次:
css
c := counter()
可以简单理解为创建了:
ini
count = 0
+
一个操作 count 的函数
执行:
scss
c()
变成:
ini
count = 1
再次:
scss
c()
变成:
ini
count = 2
因此 c 不只是一个简单函数,它还关联着自己需要使用的外部变量环境。
六、不同闭包拥有不同状态
例如:
scss
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
a := counter()
b := counter()
fmt.Println(a())
fmt.Println(a())
fmt.Println(b())
fmt.Println(a())
fmt.Println(b())
}
输出:
1
2
1
3
2
因为:
css
a := counter()
和:
css
b := counter()
分别执行了一次 counter(),每次都会创建自己的 count。
可以理解为:
css
a -> countA
b -> countB
两个闭包的状态彼此独立。
七、闭包实现计数器
闭包非常适合实现简单状态管理:
go
package main
import "fmt"
func newCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
counter := newCounter()
fmt.Println(counter())
fmt.Println(counter())
fmt.Println(counter())
}
输出:
1
2
3
外部无法直接访问:
count
只能通过返回的函数修改它。
这在一定程度上实现了数据隐藏。
八、带参数的闭包
闭包本身当然也可以接收参数:
go
func accumulator() func(int) int {
total := 0
return func(value int) int {
total += value
return total
}
}
调用:
css
add := accumulator()
fmt.Println(add(10))
fmt.Println(add(20))
fmt.Println(add(30))
输出:
10
30
60
这里 value 是每次调用时传入的参数,而:
total
是闭包持续保存的状态。
九、闭包实现自定义累加器
还可以给闭包一个初始值:
go
func accumulator(start int) func(int) int {
total := start
return func(value int) int {
total += value
return total
}
}
使用:
css
add := accumulator(100)
fmt.Println(add(10))
fmt.Println(add(20))
输出:
110
130
再创建:
css
other := accumulator(1000)
fmt.Println(other(10))
输出:
yaml
1010
每次调用 accumulator() 都会产生独立状态。
十、闭包实现函数工厂
闭包可以根据参数生成不同功能的函数。
例如:
go
func multiplier(n int) func(int) int {
return func(value int) int {
return value * n
}
}
创建:
scss
double := multiplier(2)
triple := multiplier(3)
调用:
less
fmt.Println(double(10))
fmt.Println(triple(10))
输出:
20
30
这里:
arduino
double
记住了:
ini
n = 2
而:
triple
记住了:
ini
n = 3
这种根据配置生成函数的方式通常称为函数工厂。
十一、闭包保存配置信息
例如:
go
func logger(prefix string) func(string) {
return func(message string) {
fmt.Println(prefix, message)
}
}
创建不同日志函数:
css
info := logger("[INFO]")
errorLog := logger("[ERROR]")
调用:
scss
info("服务器启动")
errorLog("连接失败")
输出:
csharp
[INFO] 服务器启动
[ERROR] 连接失败
info 自动记住:
csharp
[INFO]
而 errorLog 自动记住:
csharp
[ERROR]
调用时不需要重复传递这些配置。
十二、闭包与回调函数
上一篇学习过回调:
go
func process(nums []int, fn func(int)) {
for _, num := range nums {
fn(num)
}
}
闭包可以让回调函数携带外部状态:
go
total := 0
process([]int{10, 20, 30}, func(num int) {
total += num
})
fmt.Println(total)
输出:
60
匿名函数捕获:
total
因此在遍历过程中不断修改外部变量。
这种写法在数据处理代码中非常常见。
十三、闭包与 defer
闭包经常和 defer 一起使用。
例如:
go
func test() {
name := "Tom"
defer func() {
fmt.Println(name)
}()
name = "Jack"
}
输出:
Jack
为什么不是 Tom?
因为 defer 延迟执行的是匿名函数,匿名函数引用了外部变量 name。真正执行匿名函数时,name 已经被修改为:
Jack
如果希望保存当前值,可以通过参数传递:
go
func test() {
name := "Tom"
defer func(value string) {
fmt.Println(value)
}(name)
name = "Jack"
}
这时候输出:
Tom
理解这种区别,对下一篇学习 defer 非常重要。
十四、闭包与 goroutine
闭包还经常出现在 goroutine 中:
go
name := "Tom"
go func() {
fmt.Println(name)
}()
匿名函数引用了外部变量:
name
因此形成闭包。
不过并发环境中要更加小心,因为外部变量可能同时被其他 goroutine 修改。
例如多个 goroutine 修改:
count++
可能产生数据竞争。
因此闭包本身并不会自动保证并发安全。后面学习 Mutex、Channel 等并发知识时会详细讨论。
十五、循环中的闭包
循环配合闭包是一个需要特别注意的场景:
css
for i := 0; i < 3; i++ {
f := func() {
fmt.Println(i)
}
f()
}
这里匿名函数立即执行,所以输出:
0
1
2
如果函数被保存下来稍后执行,则需要理解它捕获的是哪个变量。现代 Go 版本已经改进了一些常见循环变量捕获行为,但阅读旧代码或处理复杂循环时,仍然应该明确变量的作用域和生命周期。
如果希望逻辑非常明确,可以显式创建局部变量:
css
for i := 0; i < 3; i++ {
value := i
func() {
fmt.Println(value)
}()
}
这样匿名函数明确捕获当前循环中的 value。
十六、闭包会影响变量生命周期
普通局部变量通常只在当前函数执行期间使用:
go
func test() {
num := 10
fmt.Println(num)
}
但是:
go
func test() func() int {
num := 10
return func() int {
return num
}
}
返回的函数仍然需要 num。
因此编译器和运行时会保证它在仍被引用期间有效。
我们不需要手动决定变量一定存放在栈还是堆上,Go 编译器会通过逃逸分析等机制进行处理。
日常开发应该重点关注:
变量是否仍然被引用
代码是否容易理解
是否产生不必要的长期引用
而不是手动管理内存。
十七、闭包不是数据复制
例如:
go
func main() {
num := 10
f := func() {
fmt.Println(num)
}
num = 100
f()
}
输出:
100
如果闭包只是把 num 创建时的值复制一份,那么结果应该是 10,但实际得到 100。
因此更准确的理解是:闭包可以捕获并引用外部变量。
再看:
go
num := 10
f := func() {
num++
}
f()
fmt.Println(num)
结果:
11
外部和闭包看到的是同一个变量。
十八、闭包实现 ID 生成器
例如需要生成连续 ID:
go
func newIDGenerator() func() int {
id := 0
return func() int {
id++
return id
}
}
使用:
css
nextID := newIDGenerator()
fmt.Println(nextID())
fmt.Println(nextID())
fmt.Println(nextID())
输出:
1
2
3
如果希望从 1000 开始:
go
func newIDGenerator(start int) func() int {
id := start
return func() int {
id++
return id
}
}
调用:
css
nextID := newIDGenerator(1000)
fmt.Println(nextID())
输出:
yaml
1001
这是闭包保存状态的典型案例。
十九、闭包实现余额管理
再看一个稍微完整的例子:
go
func account(balance float64) func(float64) float64 {
return func(amount float64) float64 {
balance += amount
return balance
}
}
使用:
css
wallet := account(100)
fmt.Println(wallet(50))
fmt.Println(wallet(-20))
fmt.Println(wallet(100))
输出:
150
130
230
balance 不需要定义成全局变量,而是由闭包维护。
不过真正的金融、订单等复杂业务不应该仅依赖简单闭包保存状态,这里主要用于理解闭包机制。
二十、什么时候适合使用闭包
闭包比较适合:
go
保存少量状态
生成不同配置的函数
实现回调
数据过滤和处理
临时封装上下文
配合 defer
配合 goroutine
例如:
go
validator := func(min int) func(int) bool {
return func(value int) bool {
return value >= min
}
}
创建:
css
adult := validator(18)
fmt.Println(adult(20))
输出:
arduino
true
闭包让 18 这个配置直接与验证函数绑定在一起。
二十一、什么时候不要滥用闭包
闭包虽然灵活,但并不是越多越好。
如果:
go
func() {
// 几十行复杂业务
// 多个数据库操作
// 大量状态修改
// 多层匿名函数
}
代码会越来越难阅读。
如果某段逻辑:
需要重复调用
业务意义明确
代码比较复杂
需要独立测试
通常更适合提取为普通函数或方法。
闭包更适合小型、局部、需要携带上下文的逻辑。
二十二、闭包常见错误
第一个错误是认为闭包保存的是变量当时的副本:
go
name := "Tom"
f := func() {
fmt.Println(name)
}
name = "Jack"
f()
结果是:
Jack
第二个错误是忽略闭包会延长相关变量的有效生命周期。闭包如果长期存在,它引用的数据也可能需要长期保留。
第三个错误是多个 goroutine 通过闭包修改同一个变量:
count++
这可能产生数据竞争。
第四个错误是为了"高级写法"大量嵌套闭包,最终降低代码可读性。
第五个错误是混淆匿名函数和闭包。匿名函数不一定是闭包,只有当函数引用了其外部作用域中的变量时,才体现出闭包特性。
二十三、匿名函数和闭包的区别
匿名函数:
go
func(a, b int) int {
return a + b
}
它没有引用外部变量,只是一个没有名字的函数。
闭包:
go
total := 0
add := func(value int) int {
total += value
return total
}
这里匿名函数引用:
total
因此形成闭包。
可以简单记住:
匿名函数:
重点是"没有名字"
闭包:
重点是"函数捕获外部变量"
闭包经常通过匿名函数实现,但两个概念并不完全等同。
二十四、总结
最简单的闭包:
go
count := 0
add := func() {
count++
}
返回闭包:
go
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
带参数:
go
func accumulator() func(int) int {
total := 0
return func(value int) int {
total += value
return total
}
}
函数工厂:
go
func multiplier(n int) func(int) int {
return func(value int) int {
return value * n
}
}
学习闭包需要重点掌握:
go
1.闭包是函数与其引用的外部环境的组合
2.闭包可以读取外部变量
3.闭包也可以修改外部变量
4.函数返回后,被闭包引用的变量仍可继续有效
5.每次调用外层函数可以创建独立的闭包状态
6.闭包适合保存少量状态和上下文
7.闭包经常用于回调、defer 和 goroutine
8.闭包本身不会自动保证并发安全
9.匿名函数和闭包不是完全相同的概念
闭包中我们已经多次看到:
go
defer func() {
fmt.Println("函数结束")
}()
而前面的文件操作中也出现过:
go
defer file.Close()
defer 看起来只是"延迟执行",实际上它还有参数求值时机、多个 defer 的执行顺序、与 return 的关系、资源释放、panic/recover 等一系列非常重要的规则。掌握这些规则,才能真正理解 Go 函数退出时发生了什么。
下一篇:defer------延迟执行、资源释放与执行顺序