当前位置: 首页 > news >正文

Golang 闭包学习

引言

在平常的 Go 语言开发中,常常需要将一段函数逻辑封装起来,异步执行、作为回调传递,甚至保持某些运行时状态。此时,闭包成为一种非常自然的编程手段。它允许我们在函数内部“记住”外部作用域中的变量,从而实现变量状态与行为绑定的能力,是函数式编程思想在 Go 中的重要体现。

这里来介绍一点闭包的场景与相关内容

闭包的定义

闭包(Closure) 是一个 函数值,它"捕获"了其外部作用域中的变量,即使这个变量在函数定义时就已经离开了原来的作用域,仍然可以被访问和使用。

换句话说:

  • 闭包 = 函数 + 外部变量的引用环境
  • 闭包就是一个"记住了外部变量"的函数,它在自己作用域外面也能继续用这些变量。

闭包的使用案例与理解

闭包的示例

func main() {a := 10add := func(x int) int {return a + x  // 闭包:引用了外部变量 a}fmt.Println(add(5)) // 输出 15
}

返回闭包的函数

func adder() func(int) int {sum := 0return func(x int) int {sum += x  // 闭包引用了外部变量 sumreturn sum}
}func main() {f := adder()fmt.Println(f(1)) // 1fmt.Println(f(2)) // 3fmt.Println(f(3)) // 6
}

这里返回的函数是一个闭包,它记住了变量 sum 的状态,即使 adder() 函数已经返回,sum 仍然存在。

变量捕获机制

这里将介绍 Go 语法作用域的一个陷阱。

场景是:你被要求首先创建一些目录,再将目录删除。

下面是我们实际使用的案例

var rmdirs []func()
for _, d := range tempDirs() {os.MkdirAll(dir, 0755) rmdirs = append(rmdirs, func() {os.RemoveAll(dir)})
}
for _, rmdir := range rmdirs {rmdir()
}
  • 其实上面的语句对于删除目录的操作是有问题的,问题的原因在于循环变量的作用域。在上面的程序中,for 循环语句引入了新的词法块,循环变量 dir 在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。需要注意,函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。
  • 以 dir 为例,后续的迭代会不断更新dir的值,当删除操作执行时,for 循环已完成,dir 中存储的值等于最后一次迭代的值。这意味着,每次对 os.RemoveAll 的调用删除的都是相同的目录。

通常,为了解决这个问题,我们会引入一个与循环变量同名的局部变量,作为循环变量的副本。比如在循环中新增一个局部变量赋值,一般是很有用的

正确的代码如下:

var rmdirs []func()
for _, d := range tempDirs() {dir := dos.MkdirAll(dir, 0755) rmdirs = append(rmdirs, func() {os.RemoveAll(dir)})
}
for _, rmdir := range rmdirs {rmdir()
}

闭包与协程结合

下面结合这样一个问题去想想改怎么实现,假设我们要处理一组任务,每个任务有自己的参数,比如一个任务 ID 或文件名,我们希望:

  • 同时并发执行(goroutine
  • 每个任务携带自己的数据
  • 任务逻辑是可配置的(函数式传入)
  • 执行完后汇总处理结果

那么用闭包实现的代码就可以是这样的

package mainimport ("fmt""sync""time"
)
type Task func() string // 定义了一个任务类型,每个任务返回一串字符串func main() {// 模拟任务的输入数据:每个 id 代表一个任务。你可以理解为模拟一批任务 ID,例如用户 ID、订单号等。taskInputs := []int{101, 102, 103, 104} // 创建一个 WaitGroup,用于等待所有 goroutine 执行完成。var wg sync.WaitGroup//创建一个缓冲通道 results,用于收集每个任务的输出结果。缓冲区大小设为任务数量,避免阻塞。results := make(chan string, len(taskInputs)) // 收集结果for _, id := range taskInputs {// 创建一个闭包(返回值是 Task 类型),闭包捕获了当前 id,绑定了任务执行逻辑task := createTask(id)// 预登记一个新的 goroutine,要等它结束wg.Add(1)/*** 启动一个 goroutine,并把 task(闭包函数)作为参数传入* goroutine 内部:* 调用 t() 执行任务* 把结果 result 发送到 results 通道* defer wg.Done() 表示任务完成**/go func(t Task) {defer wg.Done()result := t()results <- result}(task) // 在匿名函数定义完的同时立即调用它(这叫 立即执行函数 / IIFE)}// 等待所有执行完成wg.Wait()close(results)// 汇总结果fmt.Println("All task results:")for r := range results {fmt.Println(" -", r)}
}// 创建任务闭包,捕获参数 id
func createTask(id int) Task {return func() string {time.Sleep(time.Duration(id%3+1) * 300 * time.Millisecond) // 模拟任务耗时return fmt.Sprintf("Task %d done at %v", id, time.Now().Format("15:04:05.000"))}
}

假设不使用闭包的话,对于 for _, id := range taskInputs {…} 这段代码需要这样写

for _, id := range taskInputs {task := createTask(id)wg.Add(1)go runTask(task, results, &wg)
}func runTask(task Task, results chan<- string, wg *sync.WaitGroup) {defer wg.Done()result := task()results <- result
}

当然这样写也有它自己的好处

  • 没有闭包
  • 逻辑分离清晰
  • 所有变量传参,避免变量共享

缺点也是显而易见的

  • 参数多了麻烦,每次都要写 task, results, &wg,可读性变差
  • 不够灵活,不能随时在 goroutine 里“包”任意变量
  • 不适合快速临时逻辑,适合结构清晰的项目封装,但不适合快速写 demo 或内联任务

不过这里并不是一味的推荐都使用闭包来做开发,还是视场景而定,跟随团队开发风格为主,如果闭包会带来不必要的麻烦的话,建议还是使用不闭包的写法会更好一点。

其他风险

内存逃逸

使用闭包开发的同时,闭包非常容易引起内存逃逸,因为闭包常常会捕获外部变量,而这些变量需要在闭包作用域之外继续存活,就会从栈逃逸到堆

什么是逃逸?

Go 编译器会将变量优先分配在栈上(更快),但如果变量在函数返回后仍然要使用,就必须分配在堆上 —— 这就叫内存逃逸

当你创建一个闭包,并捕获了外部变量,这个变量就必须继续活着 —— 哪怕定义它的函数已经返回了,所以它不能在栈上,而要“逃逸”到堆上。

什么样的场景会容易出现内存逃逸呢?

  • 闭包作为函数返回值:如 return func() {…}
  • 闭包在 goroutine 中使用外部变量:捕获的变量生命周期超出函数
  • 闭包长期保存在结构体、全局变量中:保持状态,变量不能销毁
  • for 循环中错误引用外部变量:所有协程共享变量地址,且逃逸

实际场景

func getAdder() func(int) int {sum := 0return func(x int) int {sum += xreturn sum}
}

问题出在这个 sum 变量:

  • 本来是 getAdder 函数的局部变量
  • 但因为被闭包捕获了,并在函数返回后继续使用
  • 所以必须逃逸到堆上

另一个角度看:

闭包变量的逃逸,在这种需要持久状态的函数中,能解决吗?

  • 答案是无法“完全避免逃逸”,因为你本质上就是在保存状态,这个状态必须存活在栈之外,所以它必须分配到堆上。

但是我们可以做的事情是:

  • 理解为什么逃逸是必要的,不是 bug
  • 如果你对性能极度敏感,可以通过其他方式封装状态,让你有更清晰的生命周期管理
  • 避免不必要的逃逸,而不是逃避闭包逃逸本身

替代方案

用结构体封装状态

type Adder struct {sum int
}func (a *Adder) Add(x int) int {a.sum += xreturn a.sum
}

总结

闭包与普通函数对比

对比项普通函数闭包
是否有名字✅ 有名字(具名函数)✅ 可以是匿名或具名
是否捕获外部变量❌ 不会捕获✅ 会捕获外部变量并“记住”它们
执行上下文在调用时获取参数在定义时就绑定了外部变量环境
是否引起逃逸❌ 一般不会✅ 捕获变量时容易引起逃逸
是否适合封装状态❌ 不适合✅ 闭包可用于保存运行时状态
常见使用场景通用逻辑封装、方法实现异步任务、回调函数、延迟执行、变量记忆
示例写法func add(x, y int) intfunc() int { return x + y }

其他

  • 当你需要绑定执行时上下文(变量值)、或返回函数时,闭包非常合适;
  • 如果逻辑独立、清晰、有明确输入输出,普通函数是更稳健的选择;
  • 在协程中使用闭包时注意变量共享问题,通过参数传递或局部变量副本避免陷阱;
  • 注意闭包可能会引起变量逃逸,影响性能,必要时使用结构体封装状态更可控。

总之闭包是一种功能强大但略带隐性的语法特性,它既能提升表达力,也可能隐藏 Bug。在团队协作中,应在理解其行为的基础上谨慎使用,权衡灵活性与可维护性。真正高质量的闭包用法,是能让代码保持优雅、职责清晰,同时避免"魔法"。

相关文章:

  • 数论知识啊
  • 电子处方模块开发避坑指南:从互联网医院系统源码实践出发
  • 办公人导航网站
  • JavaWeb:HtmlCss
  • Python爬虫(3)HTML核心技巧:从零掌握class与id选择器,精准定位网页元素
  • STM32F407 HAL库使用 DMA_Normal 模式实现 UART 循环发送(无需中断)
  • 【axios取消请求】如何在token过期后取消未响应的请求
  • CSS学习笔记8——表格
  • kubernetes》》k8s》》Heml
  • 开源模型应用落地-语音合成-MegaTTS3-零样本克隆与多语言生成的突破
  • 从 Java 到 Kotlin:在现有项目中迁移的最佳实践!
  • SpringMVC知识体系
  • Java语言的进化:JDK的未来版本
  • Convenience Variable in GDB
  • 缓存穿透、雪崩、击穿深度解析与解决方案
  • 驱动开发硬核特训 · Day 19:从字符设备出发,掌握 Linux 驱动的实战路径(含 gpio-leds 控制示例)
  • oralce 查询未提交事务和终止提交事务
  • [特殊字符]️ 基于Pytest的自动化测试框架架构解析
  • 不要使用Round函数保留小数位了
  • 【问题】解决docker的方式安装n8n,找不到docker.n8n.io/n8nio/n8n:latest镜像的问题
  • 五一假期上海路网哪里易拥堵?怎么错峰更靠谱?研判报告来了
  • 美检察官向法庭提交通知,要求判处枪杀联合健康高管嫌疑人死刑
  • 上海:全面建设重复使用火箭创新高地、低成本商业卫星规模制造高地
  • 广西北海市人大常委会副主任李安洪已兼任合浦县委书记
  • 毕节两兄弟摘马蜂窝致路人被蜇去世,涉嫌过失致人死亡罪被公诉
  • 人民日报整版聚焦第十个“中国航天日”:星辰大海,再启新程