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