在go的世界中,函數是一等公民,能夠給變量賦值,能夠做爲參數傳遞,也能夠直接賦值。閉包
package main import ( "fmt" "time" ) func A() { // ... fmt.Println("this is a") } func B(f func()) { // ... } func C() func() { return A } var f func() = C() func main() { time.Sleep(time.Minute) v := C() v() }
在go語言中將這樣的變量、參數、返回值,即在堆空間和棧空間中綁定函數的值,稱爲function value異步
函數的指令在編譯期間生成,使用go tool compile -S main.go
能夠獲取彙編代碼, 以OSX 10.15.6,go 1.14爲例,將看到下述彙編代碼(下面只引用部分)函數
... "".B STEXT nosplit size=1 args=0x8 locals=0x0 0x0000 00000 (main.go:9) TEXT "".B(SB), NOSPLIT|ABIInternal, $0-8 0x0000 00000 (main.go:9) PCDATA $0, $-2 0x0000 00000 (main.go:9) PCDATA $1, $-2 0x0000 00000 (main.go:9) FUNCDATA $0, gclocals·2a5305abe05176240e61b8620e19a815(SB) 0x0000 00000 (main.go:9) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (main.go:9) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (main.go:11) PCDATA $0, $-1 0x0000 00000 (main.go:11) PCDATA $1, $-1 0x0000 00000 (main.go:11) RET 0x0000 c3 ...
運行時將存放在__TEXT段中,也就是存放在代碼段中,讀寫權限爲rx/rwx, 經過vmmap [pid]
能夠獲取運行時的內存分佈this
==== Non-writable regions for process 13443 REGION TYPE START - END [ VSIZE RSDNT DIRTY SWAP] PRT/MAX SHRMOD PURGE REGION DETAIL __TEXT 0000000001000000-0000000001167000 [ 1436K 1436K 0K 0K] r-x/rwx SM=COW .../test
使用otool -v -l [file]
能夠看到下述內容(下面只引用了一部分)spa
... Load command 1 cmd LC_SEGMENT_64 cmdsize 632 segname __TEXT vmaddr 0x0000000001000000 vmsize 0x0000000000167000 fileoff 0 filesize 1470464 maxprot rwx initprot r-x nsects 7 flags (none) Section sectname __text segname __TEXT addr 0x0000000001001000 size 0x000000000009c365 offset 4096 align 2^4 (16) reloff 0 nreloc 0 type S_REGULAR attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS reserved1 0 reserved2 0 ...
因此若是要問函數在go語言裏的本質是什麼,那麼其實就是指向__TEXT段內存地址的一個指針指針
在go語言中,每個goroutine持有一個連續棧,棧基礎大小爲2kb,當棧大小超過預分配大小後,會觸發棧擴容,也就是分配一個大小爲當前棧2倍的新棧,而且將原來的棧拷貝到新的棧上。使用連續棧而不是分段棧的目的是,利用局部性優點提高執行速度,原理是CPU讀取地址時會將相鄰的內存讀取到訪問速度比內存快的多級cache中,地址連續性越好,L一、L二、L3 cache命中率越高,速度也就越快。code
在go中,和其餘一些語言有所不一樣,函數的返回值、參數都是由被caller保存。每次函數調用時,會在caller的棧中壓入函數返回值列表、參數列表、函數返回時的PC地址,而後更改bp和pc爲新函數,執行新函數,執行完以後將變量存到caller的棧空間中,利用棧空間中保存的返回地址和caller的棧基地址,恢復pc和sp回到caller的執行過程。內存
對於棧變量的訪問是經過bp+offset的方式來訪問,而對於在堆上分配的變量來講,就是經過地址來訪問。在go中,變量被分配到堆上仍是被分配到棧上是由編譯器在編譯時根據逃逸分析決定的,不能夠更改,只能利用規則儘可能讓變量被分配到棧上,由於局部性優點,棧空間的內存訪問速度快於堆空間訪問。
get
go裏面其實方法就是語法糖,請看下述代碼,兩個Println打印的結果是同樣的,實際上Method就是將receiver做爲函數的第一個參數輸入的語法糖而已,本質上和函數沒有區別cmd
type T struct { name string } func (t T) Name() string { return "Hi! " + t.name } func main() { t := T{name: "test"} fmt.Println(t.Name()) // Hi! test fmt.Println(T.Name(t)) // Hi! test }
前面已經提到在go語言中將這在堆空間和棧空間中綁定函數的值,稱爲function value。這也就是閉包在go語言中的實體。一個最簡單的funcval其實是經過二級指針指向__TEXT代碼段上函數的結構體。
那咱們來看下面這個閉包,也就是main函數中的變量f
func getFunc() func() int { a := 0 return func() int { a++ return a } } func main() { f := getFunc() for i := 0; i < 10; i++ { fmt.Println(f()) } }
上面這段代碼執行完後會輸出1~10,也就是說f在執行的時候所使用的a會累計,可是a並非一個全局變量,爲何f就變成了一個有狀態的函數呢?其實這也就是go裏面的閉包了。那咱們來看go是如何實現閉包的。
首先來解釋一下閉包的含義,閉包在實現上一個結構體,須要存儲函數入口和關聯環境,關聯環境包含約束變量(函數內部變量)和自由變量(函數外部變量,在函數外被定義,可是在函數內被引用),和函數不一樣的事,在捕獲閉包時才能肯定自由變量,當脫離了捕捉變量的上下文時,也能照常運行。基於閉包能夠很容易的定義異步調用的回調函數。
在go語言中,閉包的狀態是經過捕獲列表實現的。具體來講,有自由變量的閉包funcval的分配都在堆上,(沒有自由變量的funcval在__DATA數據段上,和常量同樣),funcval中除了包含地址之外,還會包含所引用的自由變量,全部自由變量構成捕獲列表。對於會被修改的值,捕獲的是值的指針,對於不會被修改的值,捕獲的是值拷貝。