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

《Go语言圣经》闭包

  • 闭包(Closure)是编程语言中一个强大的特性,它允许函数访问并记住其词法作用域内的变量,即使该函数在其原始作用域之外执行。简单来说,闭包是一个函数 + 该函数捕获的外部变量的引用
  • 闭包的核心特性捕获外部变量:闭包可以访问和修改其定义时所在作用域的变量,即使该作用域已经执行完毕
  • 变量状态保持:闭包会记住它捕获的变量,这些变量不会因为其原始作用域的结束而消失。
  • 独立实例:每次创建闭包时,都会生成一个独立的变量环境副本。

例子

闭包与变量捕获

首先看squares函数的定义:

func squares() func() int {var x intreturn func() int {x++return x * x}
}

这里发生了几件重要的事情:

  1. squares函数内部声明了一个局部变量x,类型为int。在Go中,未显式初始化的变量会被赋予零值,对于int类型来说就是0

  2. squares函数返回了一个匿名函数,这个匿名函数捕获了变量x。也就是说,这个匿名函数在其生命周期内会保留对x的引用。

  3. squares函数被调用时,它返回的匿名函数会带着对x的引用一起被返回。此时,squares函数的执行已经结束,通常其局部变量的生命周期也应该结束,但由于闭包的存在,x的生命周期被延长了。

变量x的存储位置

变量x并不存储在栈上(通常函数的局部变量存储在栈上),而是存储在上。这是Go语言编译器进行逃逸分析(Escape Analysis)后的结果。当编译器发现一个变量的生命周期可能超过其所在函数的执行周期时,它会将该变量分配到堆上,而不是栈上。

在这个例子中,x的生命周期通过闭包被延长了,因此它被分配到堆上。每次调用squares()都会创建一个新的闭包实例,每个实例都有自己独立的x变量。

初始值与状态保持

当你调用squares()并将结果赋值给f时:

f := squares()

你得到了一个闭包实例,它包含了自己的x变量,初始值为0。每次调用f()时,这个x的值都会递增并计算平方:

fmt.Println(f()) // x=1, 返回1*1=1
fmt.Println(f()) // x=2, 返回2*2=4
fmt.Println(f()) // x=3, 返回3*3=9
fmt.Println(f()) // x=4, 返回4*4=16

如果再次调用squares(),会创建一个新的闭包实例,拥有自己独立的x,初始值仍然是0:

g := squares()
fmt.Println(g()) // 又是1

总结

  1. 变量位置var x int存储在堆上,因为闭包延长了它的生命周期。
  2. 初始值:Go语言中未显式初始化的变量会被赋予零值,int的零值是0
  3. 状态保持:每次调用squares()都会创建一个新的闭包实例,每个实例都有自己独立的x,其状态在多次调用之间保持。

这种闭包特性在需要维护状态的场景中非常有用,比如生成器、计数器等。

闭包可能的问题

问题核心:闭包捕获的是变量本身,而非变量的值

先看一个错误示例:

var funcs []func()
for i := 0; i < 3; i++ {funcs = append(funcs, func() {fmt.Print(i, " ") // 错误:所有闭包共享同一个i变量})
}for _, f := range funcs {f() // 输出:3 3 3(而不是0 1 2)
}

为什么输出3 3 3?

  • 闭包捕获的是变量i的内存地址,而不是循环迭代时的瞬时值。
  • 当循环结束时,i的值已经变为3,此时所有闭包再执行,读取的都是这个最终值。

正确做法:为每个闭包创建独立的变量副本

var funcs []func()
for i := 0; i < 3; i++ {j := i // 关键:创建副本,每个闭包捕获自己的jfuncs = append(funcs, func() {fmt.Print(j, " ") // 正确:输出0 1 2})
}for _, f := range funcs {f() // 输出:0 1 2
}

关键点

  • 在每次循环中,j := i创建了一个新的局部变量j,并初始化为当前i的值。
  • 每个闭包捕获的是不同的j变量(内存地址不同),因此它们的值相互独立。

再看一个字符串的例子

var greetings []func()
names := []string{"Alice", "Bob", "Charlie"}for _, name := range names {// 错误写法:所有闭包共享同一个name变量greetings = append(greetings, func() {fmt.Println("Hello,", name)})
}for _, greet := range greetings {greet() // 输出:Hello, Charlie(重复3次)
}

修正后

for _, name := range names {name := name // 创建副本greetings = append(greetings, func() {fmt.Println("Hello,", name) // 正确输出:Alice, Bob, Charlie})
}

总结

闭包捕获变量的规则:

  1. 闭包捕获的是变量本身(内存地址),而非变量的值。
  2. 如果循环内部的闭包引用了循环变量,所有闭包将共享同一个变量。
  3. 为了让每个闭包拥有独立的状态,需要在循环内部创建副本变量。

为什么Go语言要这样设计?

这是语言设计的权衡:

  • Go希望闭包捕获变量的方式简单一致(总是捕获变量本身)。
  • 开发者需要主动管理变量作用域,避免意外共享状态。

类似的问题在其他语言中也存在(如JavaScript),但解决方式可能不同(如JavaScript的let关键字会为每次循环迭代创建独立变量)。

相关文章:

  • .Net Framework 4/C# 数据访问技术(ADO.NET)
  • 技术革新赋能楼宇自控:物联网云计算推动应用前景深度拓展
  • 云计算处理器选哪款?性能与能效的平衡艺术
  • keep-alive缓存文章列表案例完整代码(Vue3)
  • keep-alive缓存文章列表案例完整代码(Vue2)
  • 汽车整车厂如何用数字孪生系统打造“透明车间”
  • King’s LIMS 系统引领汽车检测实验室数字化转型
  • 【工具使用-VScode】VScode如何设置空格和tab键显示
  • JS API接入说明
  • 多模态能解决什么样的业务场景?
  • Python内存使用分析工具深度解析与实践指南(上篇)
  • 装饰器模式深度解析:Java设计模式实战指南与动态功能扩展最佳实践
  • 《Go语言圣经》函数值、匿名函数递归与可变参数
  • NVIDIA开源Fast-dLLM!解析分块KV缓存与置信度感知并行解码技术
  • (链表:哈希表 + 双向链表)146.LRU 缓存
  • React Native【实战范例】弹跳动画菜单导航
  • 基于微信小程序的美食点餐订餐系统
  • 【Dify学习笔记】:RagFlow接入Dify基础教程
  • Flowise工作流引擎的本地部署与远程访问实践
  • Python 操作 MySQL 数据库
  • 财佰通突然做网站维护/百度网址大全 官网首页
  • 专做机票网站的软件公司/郑州关键词排名公司电话
  • 手机开发安卓app/合肥seo招聘
  • 做的很好的画册网站/今日搜索排行榜
  • 用asp做网站需要安装什么软件/交换友情链接平台
  • 网站搬家/长尾关键词举例