Golang中閉包的理解
簡介
參考部落格:
- https://www.calhoun.io/what-is-a-closure/
- https://blog.cloudflare.com/a-go-gotcha-when-closures-and-goroutines-collide/
Golang的閉包
函式在Golang中是“一等公民”,因此關於函式的特性必須要掌握號,閉包可以看成函式的高階應用,是Golang高階開發的必備技能。
匿名函式
“一等公民”意味著函式可以像普通的型別(整型、字串等)一樣進行賦值、作為函式的引數傳遞、作為函式的返回值等。Golang的函式只能返回匿名函式!
程式碼例項:
var f = func(int) {}
func main() {
f = func(i int) {
fmt.Println(i)
}
f(2)
f = func(i int) {
fmt.Println(i * i * i)
}
f(2)
}
/*
輸出:
2
8
*/
上述程式碼中,f
可以被任何輸入一個整型,無返回值的函式給賦值,這類似於C++中的函式指標。因此f
可以看成是一個函式型別的變數。這樣,可以動態的改變f
的功能。匿名函式可以動態的建立,與之成對比的常規函式必須在包中編譯前就定義完畢。匿名函式可以隨時改變功能。
閉包
閉包是匿名函式與匿名函式所引用環境的組合。匿名函式有動態建立的特性,該特性使得匿名函式不用通過引數傳遞的方式,就可以直接引用外部的變數。這就類似於常規函式直接使用全域性變數一樣,個人理解為:匿名函式和它引用的變數以及環境,類似常規函式引用全域性變數處於一個包的環境。
func main() {
n := 0
f := func() int {
n += 1
return n
}
fmt.Println(f()) // 別忘記括號,不加括號相當於地址
fmt.Println(f())
}
/*
輸出:
1
2
*/
在上述程式碼中,
n := 0
f := func() int {
n += 1
return n
}
就是一個閉包,類比於常規函式+全域性變數+包。f
不僅僅是儲存了一個函式的返回值,它同時儲存了一個閉包的狀態。
閉包作為函式返回值
匿名函式作為返回值,不如理解理解為閉包作為函式的返回值,如下程式碼:
func Increase() func() int {
n := 0
return func() int {
n++
return n
}
}
func main() {
in := Increase()
fmt.Println(in())
fmt.Println(in())
}
/*
輸出:
1
2
*/
閉包被返回賦予一個同型別的變數時,同時賦值的是整個閉包的狀態,該狀態會一直存在外部被賦值的變數in
中,直到in
被銷燬,整個閉包也被銷燬。
Golang併發中的閉包
Go語言的併發時,一定要處理好迴圈中的閉包引用的外部變數。如下程式碼:
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
fmt.Println(i)
wg.Done()
}()
}
wg.Wait()
}
輸出結果:
5
5
5
5
5
這種現象的原因在於閉包共享外部的變數i
,注意到,每次呼叫go
就會啟動一個goroutine
,這需要一定時間;但是,啟動的goroutine
與迴圈變數遞增不是在同一個goroutine
,可以把i
認為處於主goroutine
中。啟動一個goroutine
的速度遠小於迴圈執行的速度,所以即使是第一個goroutine
剛起啟動時,外層的迴圈也執行到了最後一步了。由於所有的goroutine
共享i
,而且這個i
會在最後一個使用它的goroutine
結束後被銷燬,所以最後的輸出結果都是最後一步的i==5
。
我們可以使用迴圈的延時在驗證上述說法:
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
fmt.Println(i)
wg.Done()
}()
time.Sleep(1 * time.Second) // 設定時間延時1秒
}
wg.Wait()
}
/*
輸出結果:
0
1
2
3
4
*/
每一步迴圈至少間隔一秒,而這一秒的時間足夠啟動一個goroutine
了,因此這樣可以輸出正確的結果。
在實際的工程中,不可能進行延時,這樣就沒有併發的優勢,一般採取下面兩種方法:
- 共享的環境變數作為函式引數傳遞:
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
}
/*
輸出:
4
0
3
1
2
*/
輸出結果不一定按照順序,這取決於每個goroutine
的實際情況,但是最後的結果是不變的。可以理解為,函式引數的傳遞是瞬時的,而且是在一個goroutine
執行之前就完成,所以此時執行的閉包儲存了當前i
的狀態。
2.使用同名的變數保留當前的狀態
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
i := i // 注意這裡的同名變數覆蓋
go func() {
fmt.Println(i)
wg.Done()
}()
}
wg.Wait()
}
/*
輸出結果:
4
2
0
3
1
結果順序原因同1
*/
同名的變數i
作為內部的區域性變數,覆蓋了原來迴圈中的i
,此時閉包中的變數不在是共享外迴圈的i
,而是都有各自的內部同名變數i
,賦值過程發生於迴圈goroutine
,因此保證了獨立。
相關文章
- 理解 JavaScript 中的閉包JavaScript
- 理解C#中的閉包C#
- 【譯】理解Rust中的閉包Rust
- [譯]理解JS中的閉包JS
- golang 閉包Golang
- Golang閉包Golang
- 理解“閉包”
- js閉包的理解JS
- PHP 閉包的理解PHP
- 理解Javascript的閉包JavaScript
- 談談我對js中閉包的理解JS
- 對javascript閉包的理解JavaScript
- 對JS閉包的理解JS
- 理解JavaScript 閉包JavaScript
- Groovy閉包理解
- [譯]理解閉包中的記憶體洩漏記憶體
- javascript閉包的個人理解JavaScript
- JS-閉包(closure)的理解JS
- 深入理解swift的閉包Swift
- 深入理解閉包
- Golang閉包入門瞭解Golang
- 閉包的理解-from my own opinion
- 理解Python函式閉包Python函式
- 深入理解JS閉包JS
- 用“揹包”去理解Go語言中的閉包Go
- 面試:對javascript的閉包的理解面試JavaScript
- golang中的context包GolangContext
- js中的閉包JS
- 閉包函式(匿名函式)的理解函式
- 面試題:如何理解閉包面試題
- python閉包 - 理解與應用Python
- [Python小記] 通俗的理解閉包 閉包能幫我們做什麼?Python
- JS中的 閉包(Closure)JS
- Javascript中的閉包encloureJavaScript
- 【閉包概念】關於閉包概念不同解讀——你可以自己理解。
- 【譯】理解JavaScript閉包——新手指南JavaScript
- 徹底理解閉包實現原理
- golang當中對select的理解Golang