一个Go闭包引发的血案

本文通过一个Go语言中闭包的例子,探讨了闭包如何捕获和引用外部变量,分析了为何输出结果会是9而非3。通过两种方法解释了闭包的工作原理,强调了闭包引用变量而不是复制,并提醒开发者注意变量的遮蔽效应和内存管理。
摘要由CSDN通过智能技术生成

一个Go闭包引发的血案

今天群里有人问了个问题,说是以下代码会输出什么:

func main() {
  functions := make([]func(), 3)
  for i:=0; i<3; i++ {
    functions[i] = func() {
      fmt.Println(i)
    }
  }

  functions[0]()
  functions[1]()
  functions[2]()
}

群里有人说是 0 1 2,也有人说是 3 3 3。一看这代码就知道是考闭包的,一开始我也以为是输出 0 1 2,运行了一遍后发现不对,输出的是 3 3 3

错就错在,我误以为,在表达式:

functions[i] = func() {
  fmt.Println(i)
}

中,等号右边的闭包里面,每次循环都把变量i的当前值保存下来,因此,最后的输出是 0 1 2

这个想法很荒谬,该闭包并没形式参数,里面也没有声明变量,凭什么把每次循环中变量i的值保存下来。

关于原因:群里有人说是作用域的问题。突然想起之前看过的一本书(记不清讲得是Go还是Python了),也仔细讲过闭包这一块的内容,但是具体如何说的记不清楚了。

经过一番研究,下面是我自己的理解

1. 首先看如何输出 0 1 2,然后分析为什么会输出 0 1 2

方法1——通过循环中的临时变量

for i:=0; i<3; i++ {
  i := i
  functions[i] = func() {
    fmt.Println(i)
  }
}

分析:增加了一个临时变量i,注意,fmt.Println(i)中的i是表达式i := i左边那个i,而不是for循环计数器i每次循环中,fmt.Println(i)中的i指向的都是新创建的、不同的i。该代码可以保证后续输出的是0 1 2

示意图如下:
这里写图片描述

方法2——通过高阶函数

for i:=0; i<3; i++ {
  functions[i] = (func(i int) func() {
    return func() {
      fmt.Println(i)
    }
  })(i)
}

分析: 出现了两个闭包,其中带参数的闭包,这里称为外闭包,外闭包返回的闭包,称为内闭包。在functions[i]右侧的表达式中,外闭包会立即执行,传入了变量i作为外闭包的实参,因次外闭包内部会创建一个变量i,该变量i的值会等于每次for循环中的变量i的值,return func()...返回的闭包中fmt.Println(i)所引用的变量i,是外闭包的内部变量i,而外闭包每次循环中都会创建并运行。所以呢,也是可以输出 0 1 2

2. 再回来看下原来的代码(只关注for循环):

for i:=0; i<3; i++ {
  functions[i] = func() {
    fmt.Println(i)
  }
}

for i:=0; i<3; i++i:=0这个表达式,会创建一个变量ifunctions[i]右侧表达式中的fmt.Println(i),在每次循环中,引用的都是for循环初始化中创建的变量i(只创建了一次)。循环结束后,i的值变为3,因此最终是输出的结果是3 3 3

示意图如下:
这里写图片描述

3.总结

对于循环语句中创建的闭包(或是其它情况下创建的闭包),需要明确两点:

  1. 闭包中所引用的变量到底是哪个变量,要考虑到变量的遮蔽效应。特别注意:闭包如果引用了在循环语句中的声明的变量,需要明白这些变量在每次循环中都会创建,每次都是不一样的变量(分配到不同的内存地址)
  2. 闭包对外部的变量只是引用,而不是复制,不会创建副本,当闭包中涉及该变量的表达式执行时,还得去外面找这个变量。

闭包是能够捕捉外部变量的匿名函数。这个捕捉,并不是复制,而是 “引用” ,即指向该变量的地址。例如:对本文开篇的那段代码稍微修改下:

func main() {
  functions := make([]func(), 3)
    var i int
    for i=0; i<3; i++ {
    functions[i] = func() {
        fmt.Println(i)
      }
    }

    i = 10

    functions[0]()
    functions[1]()
    functions[2]()
}

输出结果是:10 10 10 。变量i的值在创建闭包之后改变了,闭包输出的值也随之改变。

被闭包捕捉的变量因为多了一个对象引用它,只要闭包还有效,被捕捉变量所占据的内存就不会被回收,虽然它的作用域不是全局的。只要闭包活着,闭包所引用的变量就还活着。

该文章虽然分析的是Go语言中的闭包,对于其他语言,应该也适用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值