這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
對比C++,Go不支援重載和預設參數,支援不定長變參,多傳回值,匿名函數和閉包。
C入棧順序和傳回值
之前有個疑問,為什麼Go支援多傳回值,而C不行呢。首先回顧一下C函數調用時的棧空間 程式員的自我修養Ch10-2。函數調用時首先參數和返回地址入棧,其次入棧old ebp和需要儲存的寄存器,之後是函數內部的局部變數和其他資料。兩個指標ebp和esp分別指向返回地址和棧頂。
函數傳回值的傳遞有多種情況。若小於4位元組,傳回值存入eax寄存器,由函數調用方讀取eax。若傳回值5到8位元組,採用eax和edx聯合返回。若大於8個位元組,首先在棧上額外開闢一部分空間temp,將temp對象的地址做為隱藏參數入棧。函數返回時將資料拷貝給temp對象,並將temp對象的地址用寄存器eax傳出。調用方從eax指向的temp對象拷貝內容。
Go的多傳回值實現
C需要多傳回值的時候,通常是顯示的將傳回值存放的地址作為參數傳遞給函數。Go的調用慣例和C不同,Go把ret1和ret2在參數arg1 arg2之前入棧並保留空位,被呼叫者將傳回值放在這兩個空位上。
void f(int arg1, int arg2, int *ret, int *ret2)func f(arg1, arg2 int) (ret1, ret2 int)
所以無論是Go還是C,為了避免函數返回的對象拷貝,最好不要返回大對象。
匿名函數和閉包
匿名函數可以賦值給變數,作為結構體欄位,或者在channel中傳遞。匿名函數作為傳回值賦值給f變數,通過gdb調試時info locals可以查看到f變數的內容是一個地址,info symbol [addr] 可以看到這個地址指向了符號表中的main.test.func1.f符號。返回的匿名函數就是一個儲存了匿名函數地址的對象。
func test() func(int) int {return func(x int) int {x += xreturn x}}f := test()f(100) // output: 200
閉包是函數式語言的概念。同樣閉包是一個對象FuncVal{ func_addr, closure_var_point},它包含了函數地址和引用到的變數的地址。現在有個問題,如果變數x是分配在棧上的,函數test返回以後對應的棧就失效了,test返回的匿名函數中變數x將引用一個失效的位置。所以閉包環境中引用的變數不會在棧上分配。Go編譯器通過逃逸分析自動識別出變數的範圍,在堆上分配記憶體,而不是在函數f的棧上。
逃逸分析可以解釋為什麼Go可以返回局部變數的地址,而C不行。
func test() func() { x := 100 fmt.Printf("x (%p) = %d\n", &x, x) return func() { fmt.Printf("x (%p) = %d\n", &x, x) }}f := test()f() // get same output
參考文章 go基礎篇 匿名函數和閉包函數
defer 延遲調用
defer的實現
goroutine的控制結構裡有一張記錄defer運算式的表,編譯器在defer出現的地方插入了指令 call runtime.deferproc,它將defer的運算式記錄在表中。然後在函數返回之前依次從defer表中將運算式出棧執行,這時插入的指令是call runtime.deferreturn。
defer與return
defer在return之前執行的含義是:函數返回時先執行傳回值賦值,然後調用defer運算式,最後執行return。以下例子摘自go-internals,總結的都是使用defer的坑。defer確實是在return前調用的,但由於return 語句並不是原子指令,defer被插入到了賦值和ret之間,因此可能有機會改變最終的傳回值。
func f() (result int) { defer func() { // result = 0 result++ // result++ }() // return 1 return 0}
func f() (r int) { t := 5 // r = t = 5 defer func() { // t = t + 5 = 10 t = t + 5 // return 5 }() return t}
func f() (r int) { defer func(r int) { // r = 1 r = r + 5 // internal r = 6 }(r) // return 1 return 1}
這個現象是在之前做格式化error輸出的時候發現的。
defer與閉包
defer調用參數x是在defer註冊時求值或複製的,因此以下例子中x在最終調用時仍為10,而由於y是閉包參數,閉包複製的是y變數指標,因此最終y為120,實現了延遲讀取。在實際應用中還可以用指標來實現defer的延遲讀取。
fund test() { x, y := 10, 20 defer func(i int) { fmt.Println("defer:", i, y) // output: 10 120 }(x) x += 10 y += 100 fmt.Println(x, y) // output: 20 120}
defer的效能
簡單的BenchmarkTest測試發現濫用defer可能會導致效能問題,尤其在大迴圈中。
參考文章 Go學習筆記