当前位置:主页 > 查看内容

Go的闭包看你犯错,但Rust的lifetime却默默帮你排坑

发布时间:2021-07-24 00:00| 位朋友查看

简介:闭包 Closure 在某些编程语言中也被称为 Lambda 表达式 是能够读取其他函数内部变量的函数。一般只有函数内部的子函数才能读取局部变量 所以闭包这样一个函数内部的函数 在本质上是将函数内部和函数外部连接起来的桥梁。 在实践当中 假如我们需要统计一个函……

闭包 Closure 在某些编程语言中也被称为 Lambda 表达式 是能够读取其他函数内部变量的函数。一般只有函数内部的子函数才能读取局部变量 所以闭包这样一个函数内部的函数 在本质上是将函数内部和函数外部连接起来的桥梁。

在实践当中 假如我们需要统计一个函数被调用的次数 最简单的方式就是定义一个全局变量 每当目标函数被调用时就将此变量加1 但是全局变量会带来很多误用等问题 安全性往往得不到保证 而为调用次数专门设计一个以计数的接口又太小题大做了。

但是通过闭包就比较容易实现计数功能 以Go语言为例具体代码及注释如下

?package main import ( fmt func SomeFunc() func() int { // 创建一个函数 返回一个闭包 闭包每次调用函数会对函数内部变量进行累加 var CallNum int 0 //函数调用次数 系函数内部变量 外部无法访问 仅当函数被调用时进行累加 return func() int { // 返回一个闭包 CallNum //对value进行累加 //实现函数具体逻辑 return CallNum // 返回内部变量value的值 func main() { accumulator : SomeFunc() //使用accumulator变量接收一个闭包 // 累加计数并打印 fmt.Println( The first call CallNum is , accumulator()) //运行结果为 The first call CallNum is 1 // 累加计数并打印 fmt.Println( The second call CallNum is , accumulator()) //运行结果为 The second call CallNum is 2 }?

运行结果为

?The first call CallNum is 1 The second call CallNum is 2?

可以看到我们通过闭包即没有暴露CallNum这个变量 又实现了为函数计数的目的。

Goroutine 闭包却出了莫名其妙的BUG

在Go语言中 闭包所依托的匿名函数也是Goroutine所经常用到的方案之一 但是这两者一结合却容易出现极难排查的BUG 接下来我把出现问题的代码简化一下 请读者们来看下面这段代码

?import ( fmt time func main() { tests1ice : []int{1, 2, 3, 4, 5} for _, v : range tests1ice { go func() { fmt.Println(v) time.Sleep(time.Millisecond) }?

这段代码的逻辑不难看懂 其目标是通过Goroutine将1,2,3,4,5乱序输出到屏幕上 但最终执行结果却如下

?5 成功: 进程退出代码 0.?

也就是只有大多数情况下只有5被输出出来了 1-4几乎没有什么机会登场 这里简要复述一下问题的排查过程 由于没有在Goroutine中对切片执行写操作 所以首先排除了内存屏障的问题 最终还是通过反编译查看汇编代码 发现Goroutine打印的变量v 其实是地址引用 Goroutine执行的时候变量v所在地址所对应的值已经发生了变化 汇编代码如下

?for _, v : range tests1ice { 499224: 48 8d 05 f5 af 00 00 lea 0xaff5(%rip),%rax # 4a4220 type.* 0xa220 49922b: 48 89 04 24 mov %rax,(%rsp) 49922f: e8 8c 3a f7 ff callq 40ccc0 runtime.newobject 499234: 48 8b 44 24 08 mov 0x8(%rsp),%rax 499239: 48 89 44 24 48 mov %rax,0x48(%rsp) 49923e: 31 c9 xor %ecx,%ecx 499240: eb 3e jmp 499280 main.main 0xc0 499242: 48 89 4c 24 18 mov %rcx,0x18(%rsp) 499247: 48 8b 54 cc 20 mov 0x20(%rsp,%rcx,8),%rdx 49924c: 48 89 10 mov %rdx,(%rax) go func() { 49924f: c7 04 24 08 00 00 00 movl $0x8,(%rsp) 499256: 48 8d 15 f3 b7 02 00 lea 0x2b7f3(%rip),%rdx # 4c4a50 go.func.* 0x6c 49925d: 48 89 54 24 08 mov %rdx,0x8(%rsp) 499262: 48 89 44 24 10 mov %rax,0x10(%rsp) 499267: e8 54 3a fa ff callq 43ccc0 runtime.newproc ?

可Goroutine中fmt.Println所处理的v 其实是v的地址中所对应的值。这也是产生这个BUG的基本原因。

找到了问题的原因 解决起来也就简单多了。

解决方案一 在参数方式向匿名函数传递值引用 具体代码如下

package main import ( fmt time func main() { tests1ice : []int{1, 2, 3, 4, 5} for _, v : range tests1ice { go func(v int) { fmt.Println(v) }(v) time.Sleep(time.Millisecond)

解决方案二 在调用gorouinte前将变量进行值拷贝

?package main import ( fmt time func main() { tests1ice : []int{1, 2, 3, 4, 5} for _, v : range tests1ice { w : v go func() {
本文转自网络,原文链接:https://developer.aliyun.com/article/785721
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文

  • 周排行
  • 月排行
  • 总排行

随机推荐