天天看点

一个Go闭包引发的血案一个Go闭包引发的血案

一个Go闭包引发的血案

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

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

  functions]()
  functions]()
  functions]()
}
           

群里有人说是

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:; i; 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

示意图如下:

一个Go闭包引发的血案一个Go闭包引发的血案

方法2——通过高阶函数

for i:; i; 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:; i; i++ {
  functions[i] = func() {
    fmt.Println(i)
  }
}
           

for i:=0; i<3; i++

i:=0

这个表达式,会创建一个变量

i

functions[i]

右侧表达式中的

fmt.Println(i)

,在每次循环中,引用的都是

for

循环初始化中创建的变量

i

(只创建了一次)。循环结束后,

i

的值变为3,因此最终是输出的结果是

3 3 3

示意图如下:

一个Go闭包引发的血案一个Go闭包引发的血案

3.总结

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

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

闭包是能够捕捉外部变量的匿名函数

。这个

捕捉

,并不是复制,而是 “引用” ,即指向该变量的地址。例如:对本文开篇的那段代码稍微修改下:

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

    i =

    functions]()
    functions]()
    functions]()
}
           

输出结果是:

10 10 10

。变量

i

的值在创建闭包之后改变了,闭包输出的值也随之改变。

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

该文章虽然分析的是

Go

语言中的闭包,对于其他语言,应该也适用。