天天看點

一個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

語言中的閉包,對于其他語言,應該也适用。