Go 語言閉包詳解

原文連接:Go 語言閉包詳解html

前言

Go 語言閉包詳解
Go 語言閉包詳解

什麼是閉包?閉包是由函數和與其相關的引用環境組合而成的實體。golang

下面就來經過幾個例子來講明 Go 語言中的閉包以及由閉包引用產生的問題。閉包

函數變量(函數值)

在說明閉包以前,先來了解一下什麼是函數變量app

在 Go 語言中,函數被看做是第一類值,這意味着函數像變量同樣,有類型、有值,其餘普通變量能作的事它也能夠。函數

func square(x int) {
	println(x * x)
}
複製代碼
  1. 直接調用:square(1)
  2. 把函數當成變量同樣賦值:s := square;接着能夠調用這個函數變量:s(1)注意:這裏 square 後面沒有圓括號,調用纔有。
  • 調用 nil 的函數變量會致使 panic。
  • 函數變量的零值是 nil,這意味着它能夠跟 nil 比較,但兩個函數變量之間不能比較。

閉包

如今開始經過例子來講明閉包:post

func incr() func() int {
	var x int
	return func() int {
		x++
		return x
	}
}
複製代碼

調用這個函數會返回一個函數變量。ui

i := incr():經過把這個函數變量賦值給 ii 就成爲了一個閉包spa

因此 i 保存着對 x 的引用,能夠想象 i 中有着一個指針指向 xi 中有 x 的地址指針

因爲 i 有着指向 x 的指針,因此能夠修改 x,且保持着狀態:code

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 rangefor 底層實現上的不一樣。

第三個例子

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 語言聖經 - 匿名函數

相關文章
相關標籤/搜索