Go 编程实战:闭包 Closure——函数如何记住外部变量

作者:程序员爱钓鱼日期:2026/8/24

上一篇我们学习了匿名函数 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——函数如何记住外部变量》 是转载文章,点击查看原文


相关推荐


二级市场流动性“击鼓传花”全景分析:游戏逻辑、资金分工、周期规律与生存法则
初晴融雪-快雪时晴2026/8/11

核心定调:A股二级市场绝大多数短期题材炒作、热点轮动、妖股行情,本质不是价值投资,而是依托流动性的击鼓传花零和游戏。股价涨跌与企业基本面脱钩,只取决于“当下有没有人愿意用更高价格接盘”。鼓声不停、 风险提示:本文为市场规律复盘分析,不构成任何个股买卖、投资操作建议,投机炒作风险极高,普通投资者需谨慎参与。 一、基础认知:什么是股市击鼓传花游戏?(核心定义) 传统价值投资逻辑:股价=企业未来现金流折现,靠企业盈利增长、分红、估值修复赚钱。 击鼓传花投机逻辑:股价=流动性情绪+资金接力


大模型技术 提示词模板 概述
元Y亨H2026/8/2

在 LangChain 中,提示词模板(Prompt Template) 是构建大模型应用的核心基石。 如果把大模型比作一个“极其聪明但没有记忆的员工”,那么提示词模板就是一份标准化的“工作指南”。它将用户的动态输入与预设的指令、上下文、格式要求结合起来,确保大模型能输出高质量、符合预期的结果。 一、 为什么需要提示词模板? 逻辑与数据分离:你不需要在代码里硬编码复杂的提示语。你可以定义一个模板,只在运行时注入变量(如用户的问题、背景资料)。 复用性:同一个任务(如翻译、总结)的指令可以定义


我用 AI 编程半年,这 5 个 Prompt 技巧让我效率翻倍
吴琼琼2026/7/25

我用 AI 编程半年,这 5 个 Prompt 技巧让我效率翻倍 不是 AI 不行,是你的提示词太随便了。 半年前,我第一次用 AI 辅助编程的时候,体验非常糟糕。 那时候我让 AI 帮我写一个「用户管理系统」,它给我生成了 800 行代码,里面掺杂了 PHP、Python 和莫名其妙的伪代码,接口命名毫无规律,错误处理完全没有。我当时心想:这玩意儿也就这样吧。 转折点发生在一个深夜。我加班改一个数据迁移脚本,困得不行,决定再给 AI 一次机会。但这次,我没有像平时那样随口说一句「帮我写个


官方文档像天书?这本开源的 WorkBuddy"蓝皮书",或许更适合小白入门
程序员晓凡2026/7/17

它不翻译官方说明书,而是用一个个真实任务,带你从第一项工作走到一支 AI 团队。 腾讯推出的全场景 AI 办公工具 WorkBuddy算是国内AI Agent 比较优秀的产品了。 简单来说,它是一个桌面 AI Agent 工作台:你只需要用自然语言描述需求,它就能自主读取本地文件、拆解任务、调用工具,最终交付一份可验收的成果——文档、表格、PPT、数据分析报告,甚至一条完整的视频。 官方文档地址:www.codebuddy.cn/docs/workbu… 你会发现一个问题:官方文档更像是写给


如何使用 Winget 下载 Claude Code 并实现绿色便携安装
越重天2026/7/8

🧑 博主简介:CSDN博客专家,「历代文学网」(PC端可以访问:https://lidaiwenxue.com/#/?__c=1000)总架构师,首席架构师,也是联合创始人!16年工作经验,精通Java编程,高并发设计,分布式系统架构设计,Springboot和微服务,熟悉Linux,ESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友


线程概念与控制(中)
无忧.芙桃2026/6/30

本篇目标: 1.线程库的引入与理解 2.验证之前的概念 3.知道如何创建线程,终止线程,等待线程和分离线程 一.Linux线程控制 1.引入线程库 1.1.创建线程 通过之前对线程的理解,我们已经知道OS中有这么个执行流了,那么如何验证它是真的存在呢?那么就需要接下来的操作 首先我们需要创建一个线程,就需要用到pthread_create函数,如代码: 原型: int pthread_create(pthread_t *thread, const pthread_attr_


Re:Linux系统篇(三十三)文件篇·六:一文讲透 Linux 文件系统:从 EXT2 物理布局到 VFS 源码级全景解析
小此方2026/6/21

◆ 博主名称: 小此方-CSDN博客 大家好,欢迎来到小此方的博客。 ⭐️Linux系列个人专栏: 【主题曲】Linux ⭐️此方的GitHub: github_此方 ⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record) 文章目录 概要&序论一、 EXT2 文件系统的整体磁盘布局1.1 从物理磁盘到逻辑分区


Kimi版超级玛丽效果“惊人”,配额不足5厘米!
甲维斯2026/6/13

让 AI 一句话写一个《超级玛丽》的测试继续! 测完了国内最强的 GLM-5.1 之后,我们来看看国内第二强的 Kimi2.6。 Kimi 这次的表现可是“非常精彩”,我都有点不知道该如何展示! 我就先上一张图吧: 各位有何感想?这是超级玛丽?玛丽呢?管道呢?蘑菇呢? 好像已经没有什么好评了,这个结果....已经很难打分了! 我就不信这个邪了,可能真的是我运气太好了,直接抽到废卡。 我专门用它们的 Kimi Code 再跑了一遍。 这次结果好了一些,但是也很“抽象”! 能玩了,但是并不能玩多


2.PDF长文档完整读取
HappyAcmen2026/6/6

一、先知道什么是RAG 1. 一句话给 RAG 下定义 RAG = 给大模型(比如 ChatGPT、豆包、开源大模型)外挂了一个你自己的私有知识库,让大模型能精准、无差错地回答你私有数据里的内容,不会瞎编、不会用过时的知识。 你可以把 RAG 理解成: 大模型是一个超级会说话、会总结的「秘书」,但她的知识是固定的、过时的,而且完全不知道你电脑里的 PDF、文档、笔记这些私有内容RAG 就是你给这个秘书配的一个专属智能文件柜 + 精准检索神器,你把自己的所有文档、PDF、笔记都放进这个柜子里,秘书


Neo4j + Graph RAG 医疗知识图谱工程实践:患者教育问答真正需要的是“关系可追溯”
swipe2026/5/31

做医疗患者教育问答时,很多团队第一反应是把科普文章、指南摘要、院内宣教材料全部切片,然后丢进向量库。用户问“高血压要看哪个科”“为什么建议做血压监测”“糖尿病患者教育材料应该关联哪些检查”,系统就从文档里召回几段相似文本,再让大模型总结。 这条路能跑通,但很快会遇到一个问题:患者教育知识不是只有“文本相似”,还有大量“实体关系”。 比如“高血压”不是孤立的一段说明,它可能关联到常见表现、风险器官、建议就诊科室、常见检查、生活方式教育材料、随访提醒。用户真正想问的也不一定是“高血压是什么”,而是:

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读