使用函式的正確姿勢
在Go語言中,函式是一等公民,函式型別也是一等的資料型別。簡單來說,這意味著:函式不但可以用於封裝程式碼,分割功能,解耦邏輯,還可以化身為普通的值,在其他函式間傳遞、賦予變數、做型別判斷和轉換等等。
package main import "fmt" type Printer func(contents string) (n int, err error) func printToStd(contents string) (bytesNum int, err error) { return fmt.Println(contents) } func main() { var p Printer p = printToStd p("something") }
函式的什麼使用type
關鍵字。type後面是函式型別名稱,緊接著是func關鍵字。func右邊的就是這個函式型別的引數列表和結果列表。
函式的簽名就是函式的引數列表和結果列表的統稱。各個引數和結果的名稱不能算做函式簽名的一部分,甚至結果列表可以沒有名稱。嚴格來說,函式的名稱也不能算作函式簽名的一部分,它只是我們在呼叫函式時需要給定的識別符號而已。
只要兩個函式的引數列表和結果列表中的元素順序及其型別是一致的,我們就可以說它們是一樣的函式,或者說它們是實現了同一個函式型別的函式。
上面的程式碼中printToStd的簽名和Printer的是一致的,因此前者是後者的一個實現。
高階函式
Go語言在語言層面支援了函數語言程式設計。
什麼是高階函式?
滿足下面的兩個條件的任意一個的函式就是高階函式:
- 接受其他的函式作為引數傳入。
- 把其他的函式作為結果返回。
type operate func(x, y int) int
func add(x,y int)int {
return x + y
}
func calculate(x int, y int, op operate) (int, error) {
if op == nil {
return 0, errors.New("invalid operation")
}
return op(x, y), nil
}
上面的calculate函式就是一個高階函式。
實現閉包
自由變數:
在一個函式中,有一個外來識別符號,它既不是函式的引數,也不是函式的結果,更不是函式內部宣告的。它是直接從外邊拿過來的。專業屬於稱呼為“自由變數”
閉包函式就是因為引用了自由變數,而呈現出一種“不確定”的狀態,也叫“開放”狀態。也就是說閉包的內部邏輯並不是完整的,有一部分邏輯需要這個自由變數參與完成,而自由變數到底代表了什麼在閉包函式被定義的時候是未知的。在Go語言定義閉包函式的時候最多知道自由變數的型別。
type operate func(x, y int) int
type calculateFunc func(x, y int) (int, error)
func genCalculator(op operate) calculateFunc {
return func(x, y int) (i int, e error) {
if op == nil {
return 0, errors.New("invalid opration")
}
return op(x, y), nil
}
}
func main(){
op := func(x, y int) int {
return x + y
}
add := genCalculator(op)
result, err := add(3, 4)
fmt.Printf("the result: %d (error: %v)\n",result,err)
}
genCalculator函式內部就實現了閉包:那就是定義一個匿名的calculateFunc型別的函式並把它作為結果值返回。這個匿名的函式就是一個閉包函式。因為這個匿名函式裡使用的變數op既不是它的任何引數或結果,也不是它自己宣告的,而是定義它的genCalculator函式的引數,所以op在匿名函式中是一個自由變數。
這個自由變數究竟代表了什麼,這一點並不是在定義這個閉包函式的時候確定的,而是在genCalculator函式被呼叫的時候確定的。只有給定了該引數op,我們才直到它返回給我們的閉包函式可以用於什麼計算。
if op == nil
Go語言編譯器讀到這裡的時候會試圖去尋找op鎖代表的東西,它會發現op代表的是genCalculator函式的引數,然後,編譯器就會把兩者聯絡起來,這時可以說:自由變數op被“捕獲”了。當程式執行到這裡,op就是genCalculator的引數值了,這時,閉包函式的狀態就由“不確定”變為了“確定”,或者說轉到了“閉合”狀態,至此,也就真正形成了一個閉包。
閉包的意義
表面上看,我們只是延遲實現了一部分程式邏輯或功能而已。
實際上,我們是在動態地生成那部分邏輯功能。
傳入函式的引數
package main
import "fmt"
func main() {
array1 := [3]string{"a", "b", "c"}
fmt.Printf("The array: %v\n", array1)
array2 := modifyArray(array1)
fmt.Printf("The modified array: %v\n", array2)
fmt.Printf("The original array: %v\n", array1)
}
func modifyArray(a [3]string) [3]string {
a[1] = "x"
return a
}
原陣列不變。
原因是所有傳遞給函式的引數值都會被複制,函式在其內部使用的並不是引數值的原值,而是它的副本。
注意,對於引用型別,切片,字典,通道,複製的是它們本身的值,不會複製它們引用的底層資料,也就是說,這只是淺表複製,不是深層複製。Go語言沒有深層複製。
對於值型別的陣列,如果它的元素型別是引用型別,當這種陣列被傳入函式的話,函式中對該引數值的修改會影響到引數嗎?例如[3][]string
如果修改陣列中的切片的某個元素,會影響原陣列;如果修改陣列的某個元素就不會影響原陣列。
函式返回給呼叫方的結果值會被複制嗎?
當函式返回指標型別時,不會發生拷貝。當函式返回非指標型別並把結果賦值給其他變數時,會發生拷貝。