Golang中閉包的理解

Erick_Lv發表於2018-08-24

簡介

參考部落格:

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了,因此這樣可以輸出正確的結果。

在實際的工程中,不可能進行延時,這樣就沒有併發的優勢,一般採取下面兩種方法:

  1. 共享的環境變數作為函式引數傳遞:
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,因此保證了獨立。

相關文章