Go 语言闭包详解

Go 语言闭包详解及常见应用 三分钟搞懂闭包和逃逸分析 阅读详情

Go 语言闭包详解

原文链接:Go 语言闭包详解

前言

 

Go 语言闭包详解

Go 语言闭包详解

 

什么是闭包?闭包是由函数和与其相关的引用环境组合而成的实体。

下面就来通过几个例子来说明 Go 语言中的闭包以及由闭包引用产生的问题。

函数变量(函数值)

在说明闭包之前,先来了解一下什么是函数变量。

在 Go 语言中,函数被看作是第一类值,这意味着函数像变量一样,有类型、有值,其他普通变量能做的事它也可以。

func square(x int) {
	println(x * x)
}
复制代码
  1. 直接调用:square(1)
  2. 把函数当成变量一样赋值:s := square;接着可以调用这个函数变量:s(1)。 注意:这里 square 后面没有圆括号,调用才有。
  • 调用 nil 的函数变量会导致 panic。
  • 函数变量的零值是 nil,这意味着它可以跟 nil 比较,但两个函数变量之间不能比较。

闭包

现在开始通过例子来说明闭包:

func incr() func() int {
	var x int
	return func() int {
		x++
		return x
	}
}
复制代码

调用这个函数会返回一个函数变量。

i := incr():通过把这个函数变量赋值给 i,i 就成为了一个闭包。

所以 i 保存着对 x 的引用,可以想象 i 中有着一个指针指向 x 或 i 中有 x 的地址。

由于 i 有着指向 x 的指针,所以可以修改 x,且保持着状态:

println(i()) // 1
println(i()) // 2
println(i()) // 3
复制代码

也就是说,x 逃逸了,它的生命周期没有随着它的作用域结束而结束。

但是这段代码却不会递增:

println(incr()()) // 1
println(incr()()) // 1
println(incr()()) // 1
复制代码

这是因为这里调用了三次 incr(),返回了三个闭包,这三个闭包引用着三个不同的 x,它们的状态是各自独立的。

闭包引用

现在开始通过例子来说明由闭包引用产生的问题:

x := 1
f := func() {
	println(x)
}
x = 2
x = 3
f() // 3
复制代码

因为闭包对外层词法域变量是引用的,所以这段代码会输出 3。

可以想象 f 中保存着 x 的地址,它使用 x 时会直接解引用,所以 x 的值改变了会导致 f 解引用得到的值也会改变。

但是,这段代码却会输出 1:

x := 1
func() {
	println(x) // 1
}()
x = 2
x = 3
复制代码

把它转换成这样的形式就容易理解了:

x := 1
f := func() {
	println(x)
}
f() // 1
x = 2
x = 3
复制代码

这是因为 f 调用时就已经解引用取值了,这之后的修改就与它无关了。

不过如果再次调用 f 还是会输出 3,这也再一次证明了 f 中保存着 x 的地址。

可以通过在闭包内外打印所引用变量的地址来证明:

x := 1
func() {
	println(&x) // 0xc0000de790
}()
println(&x) // 0xc0000de790
复制代码

可以看到引用的是同一个地址。

循环闭包引用

接下来在三个例子中说明由循环内的闭包引用所产生的问题:

第一个例子

for i := 0; i < 3; i++ {
	func() {
		println(i) // 0, 1, 2
	}()
}
复制代码

这段代码相当于:

for i := 0; i < 3; i++ {
	f := func() {
		println(i) // 0, 1, 2
	}
	f()
}
复制代码

每次迭代后都对 i 进行了解引用并使用得到的值且不再使用,所以这段代码会正常输出。

第二个例子

正常代码:输出 0, 1, 2:

var dummy [3]int
for i := 0; i < len(dummy); i++ {
	println(i) // 0, 1, 2
}
复制代码

然而这段代码会输出 3:

var dummy [3]int
var f func()
for i := 0; i < len(dummy); i++ {
	f = func() {
		println(i)
	}
}
f() // 3
复制代码

前面讲到闭包取引用,所以这段代码应该输出 i 最后的值 2 对吧?

不对。这是因为 i 最后的值并不是 2。

把循环转换成这样的形式就容易理解了:

var dummy [3]int
var f func()
for i := 0; i < len(dummy); {
	f = func() {
		println(i)
	}
	i++
}
f() // 3
复制代码

i 自加到 3 才会跳出循环,所以循环结束后 i 最后的值为 3。

所以用 for range 来实现这个例子就不会这样:

var dummy [3]int
var f func()
for i := range dummy {
	f = func() {
		println(i)
	}
}
f() // 2
复制代码

这是因为 for range 和 for 底层实现上的不同。

第三个例子

var funcSlice []func()
for i := 0; i < 3; i++ {
	funcSlice = append(funcSlice, func() {
		println(i)
	})

}
for j := 0; j < 3; j++ {
	funcSlice[j]() // 3, 3, 3
}
复制代码

输出序列为 3, 3, 3。

看了前面的例子之后这里就容易理解了: 这三个函数引用的都是同一个变量(i)的地址,所以之后 i 递增,解引用得到的值也会递增,所以这三个函数都会输出 3。

添加输出地址的代码可以证明:

var funcSlice []func()
for i := 0; i < 3; i++ {
	println(&i) // 0xc0000ac1d0 0xc0000ac1d0 0xc0000ac1d0
	funcSlice = append(funcSlice, func() {
		println(&i)
	})

}
for j := 0; j < 3; j++ {
	funcSlice[j]() // 0xc0000ac1d0 0xc0000ac1d0 0xc0000ac1d0
}
复制代码

可以看到三个函数引用的都是 i 的地址。

解决方法

1. 声明新变量:

  • 声明新变量:j := i,且把之后对 i 的操作改为对 j 操作。
  • 声明新同名变量:i := i。注意:这里短声明右边是外层作用域的 i,左边是新声明的作用域在这一层的 i。原理同上。

这相当于为这三个函数各声明一个变量,一共三个,这三个变量初始值分别对应循环中的 i 并且之后不会再改变。

2. 声明新匿名函数并传参:

var funcSlice []func()
for i := 0; i < 3; i++ {
	func(i int) {
		funcSlice = append(funcSlice, func() {
			println(i)
		})
	}(i)

}
for j := 0; j < 3; j++ {
	funcSlice[j]() // 0, 1, 2
}
复制代码

现在 println(i) 使用的 i 是通过函数参数传递进来的,并且 Go 语言的函数参数是按值传递的。

所以相当于在这个新的匿名函数内声明了三个变量,被三个闭包函数独立引用。原理跟第一种方法是一样的。

这里的解决方法可以用在大多数跟闭包引用有关的问题上,不局限于第三个例子。

参考链接

Go 语言圣经 - 匿名函数

Go语言核心特性:指针、init函数、闭包与defer详解 指针作为编程语言中的基础概念,通过直接操作内存地址实现高效数据访问。Go语言在保留指针强大功能的同时,通过类型安全、自动内存管理等机制降低了使用风险。理解指针原理有助于优化大对象传递、实现接口方法等场景。init函数是Go特有的初始化机制,通过严格的执行顺序保证程序正确启动。闭包将函数与引用环境结合,在中间件、资源管理等场景表现优异。defer语句以FILO顺序执行延迟调用,是资源清理的理想选择。本文深入解析这些Go核心特性,结合内存逃逸分析、并发安全等工程实践,帮助开发者规避常见陷阱,提升代码质量与性能。 阅读详情

相关推荐

《惠普工作站设置BIOS从U盘装系统》

惠普工作站设置BIOS 惠普工作站在做服务器时应用广泛,本文介绍下其BIOS开机自启动设置,附上图片,以供学习参考。 惠普工作站设置BIOS 启动步骤如下: 第一步:根据需要对BIOS进行更改,并保存 1.重启工作站按F10进入BIOS设置(也可以按ESC键选择进入BIOS设置的选项)。 图1 惠普工作站B...

istrangeboy&begin zero的博客 2万+

聊聊Go里面的闭包

聊聊Go里面的闭包......

秦怀杂货店 4289

AD服务器搭建

一、角色安装1)打开服务器管理器,添加 角色和功能;2) 点 下一步;3)点 下一步;4)点 下一步;5)点击安装;6-1)将此服务器提升为域控制器;6-2)点下一步;6)输入密码 点 下一步;7)自动生成LIXF,点击下一步;8)确认目录,点下一步;8)确认安装条件,点安装; 8)安装成功,点关闭;...

潇峰的博客 6452

Go语言之闭包

Go语言之闭包(闭包原理)

Sheena997的博客 1498

layui 监听单元格点击事件,获取当前点击单元格的内容以及 所在的表头内容

特别是后台的一些管理系统中 业务中还是会碰见这样的需求呢 如果你的后台使用layui写的话 在表格中 layui 也给我们提供了监听单元格的事件 但是好像不能直接获取到单元格内容个 和 对应的表头 其实 还是可以解决 换个角度使劲就会发现新的世界 不是吗 event: 'username' 在每个表头中加一个 event 属性 这样就可以监听表头事件 记住个 event 和 当前列的字段最好保持一样 table.render({ e...

业余前端的职业经历 1万+

SpringBoot+Vue前后端分离项目实战:个人博客系统

MyBlog主要是基于 SpringBoot + Vue 前后端分离开发的一款动态个人博客系统,后台和前台均通过JSON数据做交互,主要功能有文章管理、分类管理、标签管理、附件管理、评论管理、友链管理、个人资料/博客信息管理等。

Myles'Blog 1万+

【go语言基础】go语言中的闭包

详解go语言中闭包

CodingLJ 8762

白话FOC控制中各环节的参数的意义与如何设置参数

FOC参数整定是控制系统的关键环节,如同驯马需要调节"智能马鞍"。核心参数分为三类:电流环参数(P/I/D)决定响应速度和稳定性,速度环参数需慢于电流环5-10倍带宽,系统参数(极对数、电阻等)是电机固有属性必须准确设置。调参心法:由内而外先调电流环,遵循先P后I慎用D原则,内环带宽必须大于外环。利用自动辨识工具,通过听觉(滋滋声表示P过大)和波形观察来微调,最终实现精准控制。

汽车软件开发 356

Go 函数式编程(闭包)

一 函数式编程概论 在过去近十年时间里,面向对象编程大行其道,以至于在大学的教育里,老师也只会教给我们两种编程模型,面向过程和面向对象。孰不知,在面向对象思想产生之前,函数式编程已经有了数十年的历史。就让我们回顾这个古老又现代的编程模型,看看究竟是什么魔力将这个概念在21世纪的今天再次拉入我们的视野。 随着硬件性能的提升以及编译技术和虚拟机技术的改进,一些曾被性能问题所限制的动态语言开

Anonymous 972

uniapp [全端兼容] - 实现内网使用天地图离线地图,详解项目加载天地图的离线地图服务器部署与使用教程示例源码,Uniapp如何实现天地图的离线加载部署在内网访问(兼容H5、微信小程序、App)

uniapp天地图如何离线访问,无需网络调用天地图API,天地图离线地图,天地图——内网使用离线地图,uniapp中加载天地图的离线地图,uniAPP实现天地图离线访问,在内网中无法直接使用外网天地图资源怎么办,在uni-app中加载地图 -使用天地图的瓦片数据。UNIAPP天地图离线怎么使用,内网部署天地图,天地图离线瓦片,部分瓦片数据,天地图如何离线,下载天地图离线服务,并用Uniapp+天地图离线部署,天地图私有化部署,实现离线加载天地图,Java+uniapp天地图离线开发,uniapp项目中无网络

🏆 前端领域优质创作者 4904

VS code 调试显示preLaunchTask“g++“已终止,退出代码为-1。 生成‘... .exe‘文件错误

总结: 以后每个工程都需要按照这样的流程:创建一个.c/.cpp文件 -> F5运行 -> 配置tasks.json文件-> launch.json文件 -> 运行

qq_45151652的博客 2516

闭包的知识

闭包是带有环境变量引用的函数,可以保存和使用创建时的上下文数据。这种特性使得闭包在需要记住函数状态、创建自定义函数工厂、计数器等场景中非常有用。闭包常用于创建不同配置的函数。我们可以在闭包中定义一部分行为,让函数调用者提供另一部分参数,从而实现定制化的功能。// filter 函数接收一个整数切片和一个闭包函数,用于过滤符合条件的元素if test(num) { // 调用闭包判断条件// 传递闭包函数来筛选出偶数})

2301_76723322的博客 1339

【PyTorch项目实战】之ResNet系列:resnet18、resnet34、resnet50、resnet101、resnet152

ResNet (Residual Network) :是一种深度卷积神经网络(CNN)架构,其核心思想是引入残差学习(Residual Learning)机制,通过跳跃连接(skip connections)来解决深层网络训练中的退化问题。

📷 图像处理践行者 | 传统算法 · 深度学习 · 多模态 | 分享实战经验与技术洞见,探索从像素到智能的无限可能。 6375

Go语言中的闭包详解

Go语言闭包解析:闭包是由函数及其引用环境组成的实体,能够访问并修改外部作用域变量,即使外部函数已执行完毕。关键特性包括:1)捕获外部变量;2)保持状态(变量生命周期延长);3)隔离性(每次调用创建独立实例)。典型应用场景包括状态封装、中间件实现和资源管理。需注意循环变量捕获的陷阱,可通过参数传递或立即执行解决。示例展示了闭包实现指数增长、函数工厂和并发隔离等功能,体现了闭包在状态保持和逻辑封装方面的优势。

念旧的博客 1094

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

Golang 语言函数闭包详解

访问和修改外部作用域的变量保持状态 between calls创建封装的行为单元正确使用闭包可以使代码更加简洁和表达力强,但需要注意避免常见的陷阱,特别是循环中的变量捕获和潜在的内存泄漏问题。

学亮编程手记 559

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

Go 语言闭包详解:从基础到实践

闭包是函数 + 引用环境的组合捕获的是变量本身,不是值拷贝延长外部变量的生命周期。

qq_38609643的博客 951

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

【Go 基础篇】Go语言闭包详解:共享状态与函数式编程

在Go语言中,闭包是一种强大的编程特性,它允许函数内部包含对外部作用域变量的引用。闭包使得函数可以捕获和共享外部作用域的状态,实现更加灵活和复杂的编程模式。本篇博客将深入探讨Go语言中闭包的概念、用法、实现原理以及在函数式编程中的应用。闭包(Closure)是指一个函数包含了它外部作用域中的变量,即使在外部作用域结束后,这些变量依然可以被内部函数访问和修改。闭包使得函数可以“记住”外部作用域的状态,这种状态在函数调用之间是保持的。闭包的核心概念是函数内部可以引用外部作用域的变量。

繁依Fanyi的博客 1167

Go语言实践[回顾]教程26--详解Go语言函数的闭包

闭包,其他很多开发语言也有,但是很多开发者对闭包感觉很难懂,也很难用,甚至干脆弃之不用。但既然很多语言包括 Go 语言也提供了闭包这种语法,就肯定有其必要性和某些适用场景。这一节,我们就实践一下 Go 语言的闭包,希望可以了解其特点,理解其原理,知道其适用场景,以便在编程开发中用好闭包,为编程扩展思路发挥作用。

IT技术实战 755

go语言函数、方法、闭包(详解)

详细的介绍了Go语言函数、方法的使用方法,包括多参数,多返回值使用方法,以及函数闭包的使用。

一个非常Cool的人 978

Go语言闭包详解

人们常说面向对象编程和面向过程编程,随着硬件性能的提升以及编译技术和虚拟机技术的改进,一些曾被性能问题所限制的动态语言开始受到关注,Python、Ruby 和 Lua 等语言都开始在应用中崭露头角。动态语言因其方便快捷的开发方式成为很多人喜爱的编程语言,伴随动态语言的流行,函数式编程也再次进入了我们的视野。 那究竟什么是函数式编程呢? 简单来说,函数式编程是一种编程模型,他将计算机运算看...

码墨 497

Go 语言闭包(Closure)详解

闭包 = 函数 + 捕获的外部变量捕获的变量在闭包内是引用,修改会影响外部变量循环中使用闭包需注意变量捕获问题Go 中闭包常用于累加器、回调和函数工厂理解闭包是深入掌握 Go 函数式编程风格的关键。

lihaoweiwudi_的博客 699

Go 语言函数值与函数闭包详解:从基础到实战

本文聚焦于 Go 语言中的函数值和函数闭包,深入讲解其概念、特性与使用场景。通过丰富的代码示例和实际项目案例,让读者清晰掌握如何运用函数值和闭包来编写灵活、高效的代码。同时,对相关重点知识进行拓展,剖析使用时的注意事项。希望读者通过本文学习,能在 Go 编程中熟练运用函数值和闭包,提升编程能力。本文详细介绍了 Go 语言中的函数值和函数闭包。函数值让函数可以像变量一样被灵活使用,可作为参数传递和返回值;函数闭包则能访问并修改其外部函数的变量,实现状态保存和复杂逻辑。

Tekin 是深耕技术 20 年的全栈实战派专家,精通 Go/Python/Java 等多语言开发。博客专注技术原理与实战结合,深度解析 Python 高阶编程、Go 语言架构、数据库优化等硬核内容。涵盖并发编程、机器学习、云原生等前沿领域,通过真实案例拆 1132

【GO语言04-1】Go语言中关于函数定义、闭包函数、作用域、defer关键字详解

Go语言中的功能封装是通过函数进行的,不同结构体之间可以通过接口来进行统一,再结合反射特性就可以开发大型的、复杂的项目。 一、函数的定义 Go语言是支持面向对象编程的,但是函数才是Go语言的基本组成元素。 Go语言的函数分为具名函数、匿名函数 //具名函数 func main(a int) int { return a * a } main(11) //匿名函数 var nua = func(a int) int { return a * a } nua(12) 二、闭包 匿名函数可以赋值给一个变量

量化Mike 1024

【Go 基础篇】Go语言匿名函数详解:灵活的函数表达式与闭包

在Go语言中,函数是一等公民,这意味着函数可以像其他类型的值一样被操作、传递和赋值。匿名函数是一种特殊的函数,它没有固定的函数名,可以在代码中被直接定义和使用。匿名函数在Go语言中具有重要的地位,它们常用于实现闭包、函数式编程和并发编程等领域。本篇博客将深入探讨Go语言中匿名函数的概念、定义方式、使用场景以及与闭包的关系。通过学习匿名函数,您将能够更加灵活地使用函数表达式,编写更具表达力和可维护性的代码。匿名函数,也称为函数字面值或函数表达式,是一种没有名称的函数。

繁依Fanyi的博客 1185
上一篇: k8s源码分析--kube-scheduler源码(一)
下一篇: IO多路复用的三种机制Select,Poll,Epoll
cbmljs
博客等级 码龄10年 329粉丝 130原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值