您好,登錄后才能下訂單哦!
這意味著函數不但可以用于封裝代碼、分割功能、解耦邏輯,還可以化身為普通的值,在其他函數間傳遞、賦予變量、做類型判斷和轉換等等,就像切片和字典的值那樣。
而更深層次的含義就是:函數值可以由此成為能夠被隨意傳播的獨立邏輯組件(或者說功能模塊)。
對于函數類型來說,它是一種對一組輸入、輸出進行模板化的重要工具,它比接口類型更加輕巧、靈活,它的值也借此變成了可被熱替換的邏輯組件。
我先聲明了一個函數類型,名叫Printer,注意這里的寫法,在類型聲明的名稱右邊的是func關鍵字,我們由此就可知道這是一個函數類型的聲明。
nc右邊的就是這個函數類型的參數列表和結果列表。其中,參數列表必須由圓括號包裹,而只要結果列表中只有一個結果聲明,并且沒有為它命名,我們就可以省略掉外圍的圓括號。
書寫函數簽名的方式與函數聲明的是一致的。只是緊挨在參數列表左邊的不是函數名稱,而是關鍵字func。這里函數名稱和func互換了一下位置而已
函數的簽名其實就是函數的參數列表和結果列表的統稱,它定義了可用來鑒別不同函數的那些特征,同時也定義了我們與函數交互的方式。
注意,各個參數和結果的名稱不能算作函數簽名的一部分,甚至對于結果聲明來說,沒有名稱都可以。只要兩個函數的參數列表和結果列表中的元素順序及其類型是一致的,我們就可以說它們是一樣的函數,或者說是實現了同一個函數類型的函數。嚴格來說,函數的名稱也不能算作函數簽名的一部分,它只是我們在調用函數時,需要給定的標識符而已。
聲明的函數printToStd的簽名與Printer的是一致的,因此前者是后者的一個實現,即使它們的名稱以及有的結果名稱是不同的。
通過main函數中的代碼,我們就可以證實這兩者的關系了,我順利地把printToStd函數賦給了Printer類型的變量p,并且成功地調用了它。
總之,“函數是一等的公民”是函數式編程(functional programming)的重要特征。Go 語言在語言層面支持了函數式編程
package main
import "fmt"
//先聲明了一個函數類型,名叫Printer,函數簽名:函數的參數列表和結果列表的統稱
type Printer func(contents string) (n int, err error)
//定義了一個函數,printToStd的簽名與Printer的是一致的,因此printToStd是Printer的一個實現,即使它們的名稱以及有的結果名稱是不同的
func printToStd(contents string) (bytesNum int, err error) {
return fmt.Println(contents)
}
func main() {
var p Printer //初始化一個Printer 類型的p
p = printToStd //順利地把printToStd函數賦給了Printer類型的變量p,并且成功地調用了它
p("something")
}
go run demo26.go
something
什么是高階函數?只要滿足了其中任意一個特點,我們就可以說這個函數是一個高階函數
我想通過編寫calculate函數來實現兩個整數間的加減乘除運算,但是希望兩個整數和具體的操作都由該函數的調用方給出,那么,這樣一個函數應該怎樣編寫呢。
我們編寫calculate函數的簽名部分。這個函數除了需要兩個int類型的參數之外,還應該有一個operate類型的參數。該函數的結果應該有兩個,一個是int類型的,代表真正的操作結果,另一個應該是error類型的,因為如果那個operate類型的參數值為nil,那么就應該直接返回一個錯誤
函數類型屬于引用類型,它的值可以為nil,而這種類型的零值恰恰就是nil。
calculate函數實現起來就很簡單了。我們需要先用衛述語句檢查一下參數,如果operate類型的參數op為nil,那么就直接返回0和一個代表了具體錯誤的error類型值。
衛述語句是指被用來檢查關鍵的先決條件的合法性,并在檢查未通過的情況下立即終止當前代碼塊執行的語句。在 Go 語言中,if 語句常被作為衛述語句。如果檢查無誤,那么就調用op并把那兩個操作數傳給它,最后返回op返回的結果和代表沒有錯誤發生的nil。
calculate函數的其中一個參數是operate類型的,而且后者就是一個函數類型。在調用calculate函數的時候,我們需要傳入一個operate類型的函數值。這個函數值應該怎么寫?
只要它的簽名與operate類型的簽名一致,并且實現得當就可以了。我們可以像上一個例子那樣先聲明好一個函數,再把它賦給一個變量,也可以直接編寫一個實現了operate類型的匿名函數。
calculate函數就是一個高階函數。但是我們說高階函數的特點有兩個,而該函數只展示了其中一個特點,即:接受其他的函數作為參數傳入。
那另一個特點,把其他的函數作為結果返回。這又是怎么玩的呢?你可以看看我在 demo27.go 文件中聲明的函數類型calculateFunc和函數genCalculator。其中,genCalculator函數的唯一結果的類型就是calculateFunc
package main
import (
"errors"
"fmt"
)
type operate func(x, y int) int //我們來聲明一個名叫operate的函數類型,它有兩個參數和一個結果,都是int類型的。
// 方案1。calculate函數就是一個高階函數。該函數只展示了其中一個特點,即:接受其他的函數作為參數傳入。
func calculate(x int, y int, op operate) (int, error) {
if op == nil { //衛述語句檢查op的合法性
return 0, errors.New("invalid operation")
}
return op(x, y), nil
}
// 方案2。calculateFunc也是高階函數,把其他的函數op作為結果返回
type calculateFunc func(x int, y int) (int, error)
func genCalculator(op operate) calculateFunc {
return func(x int, y int) (int, error) {
if op == nil {
return 0, errors.New("invalid operation")
}
return op(x, y), nil
}
}
func main() {
// 方案1。
x, y := 12, 23
op := func(x, y int) int {
return x + y
}
result, err := calculate(x, y, op) //把函數op作為一個普通的值賦給一個變量。
fmt.Printf("The result: %d (error: %v)\n",
result, err)
result, err = calculate(x, y, nil)
fmt.Printf("The result: %d (error: %v)\n",
result, err)
// 方案2。
x, y = 56, 78
add := genCalculator(op)
result, err = add(x, y)
fmt.Printf("The result: %d (error: %v)\n",
result, err)
}
go run demo27.go
The result: 35 (error: <nil>)
The result: 0 (error: invalid operation)
The result: 134 (error: <nil>)
閉包又是什么?你可以想象一下,在一個函數中存在對外來標識符的引用。所謂的外來標識符,既不代表當前函數的任何參數或結果,也不是函數內部聲明的,它是直接從外邊拿過來的。
還有個專門的術語稱呼它,叫自由變量,可見它代表的肯定是個變量。實際上,如果它是個常量,那也就形成不了閉包了,因為常量是不可變的程序實體,而閉包體現的卻是由“不確定”變為“確定”的一個過程。
我們說的這個函數(以下簡稱閉包函數)就是因為引用了自由變量,而呈現出了一種“不確定”的狀態,也叫“開放”狀態。
也就是說,它的內部邏輯并不是完整的,有一部分邏輯需要這個自由變量參與完成,而后者到底代表了什么在閉包函數被定義的時候卻是未知的。
即使對于像 Go 語言這種靜態類型的編程語言而言,我們在定義閉包函數的時候最多也只能知道自由變量的類型
在我們剛剛提到的genCalculator函數內部,實際上就實現了一個閉包,而genCalculator函數也是一個高階函數。
genCalculator函數只做了一件事,那就是定義一個匿名的、calculateFunc類型的函數并把它作為結果值返回。
而這個匿名的函數就是一個閉包函數。它里面使用的變量op既不代表它的任何參數或結果也不是它自己聲明的,而是定義它的genCalculator函數的參數,所以是一個自由變量。
這個自由變量究竟代表了什么,這一點并不是在定義這個閉包函數的時候確定的,而是在genCalculator函數被調用的時候確定的。只有給定了該函數的參數op,我們才能知道它返回給我們的閉包函數可以用于什么運算。
看到if op == nil {那一行了嗎?Go 語言編譯器讀到這里時會試圖去尋找op所代表的東西,它會發現op代表的是genCalculator函數的參數,然后,它會把這兩者聯系起來。這時可以說,自由變量op被“捕獲”了。
當程序運行到這里的時候,op就是那個參數值了。如此一來,這個閉包函數的狀態就由“不確定”變為了“確定”,或者說轉到了“閉合”狀態,至此也就真正地形成了一個閉包。
看出來了嗎?我們在用高階函數實現閉包。這也是高階函數的一大功用。
(高階函數與閉包)
那么,實現閉包的意義又在哪里呢?表面上看,我們只是延遲實現了一部分程序邏輯或功能而已,但實際上,我們是在動態地生成那部分程序邏輯。
我們可以借此在程序運行的過程中,根據需要生成功能不同的函數,繼而影響后續的程序行為。這與 GoF 設計模式中的“模板方法”模式有著異曲同工之妙,不是嗎?
這個命令源碼文件(也就是 demo28.go示例一)在運行之后會輸出什么?
答案是:原數組不會改變。為什么呢?原因是,所有傳給函數的參數值都會被復制,函數在其內部使用的并不是參數值的原值,而是它的副本。由于數組是值類型,所以每一次復制都會拷貝它,以及它的所有元素值。我在modify函數中修改的只是原數組的副本而已,并不會對原數組造成任何影響。
對于引用類型,比如:切片、字典、通道,像上面那樣復制它們的值,只會拷貝它們本身而已,并不會拷貝它們引用的底層數據。也就是說,這時只是淺表復制,而不是深層復制。
以切片值為例,如此復制的時候,只是拷貝了它指向底層數組中某一個元素的指針,以及它的長度值和容量值,而它的底層數組并不會被拷貝。
另外還要注意,就算我們傳入函數的是一個值類型的參數值,但如果這個參數值中的某個元素是引用類型的,那么我們仍然要小心。
變量complexArray1是[3][]string類型的,也就是說,雖然它是一個數組,但是其中的每個元素又都是一個切片。這樣一個值被傳入函數的話,函數中對該參數值的修改會影響到complexArray1本身嗎?我想,這可以留作今天的思考題。
package main
import "fmt"
func main() {
// 示例1。底層數組不會被修改,所有傳給函數的參數值都會被復制,函數在其內部使用的并不是參數值的原值,而是它的副本
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)
fmt.Println()
// 示例2。切片會被修改掉,但是切片底層的數組不變
slice1 := []string{"x", "y", "z"}
fmt.Printf("The slice: %v\n", slice1)
slice2 := modifySlice(slice1)
fmt.Printf("The modified slice: %v\n", slice2)
fmt.Printf("The original slice: %v\n", slice1)
fmt.Println()
// 示例3。 /切片被修改,底層數組不變
complexArray1 := [3][]string{
[]string{"d", "e", "f"},
[]string{"g", "h", "i"},
[]string{"j", "k", "l"},
}
fmt.Printf("The complex array: %v\n", complexArray1)
complexArray2 := modifyComplexArray(complexArray1) //切片被修改,底層數組不變
fmt.Printf("The modified complex array: %v\n", complexArray2)
fmt.Printf("The original complex array: %v\n", complexArray1)
}
// 示例1。
func modifyArray(a [3]string) [3]string {
a[1] = "x"
return a
}
// 示例2。
func modifySlice(a []string) []string {
a[1] = "i"
return a
}
// 示例3。
func modifyComplexArray(a [3][]string) [3][]string {
a[1][1] = "s"
a[2] = []string{"o", "p", "q"}
return a
}
go run demo28.go
The array: [a b c]
The modified array: [a x c]
The original array: [a b c]
The slice: [x y z]
The modified slice: [x i z]
The original slice: [x i z]
The complex array: [[d e f] [g h i] [j k l]]
The modified complex array: [[d e f] [g s i] [o p q]] //切片被修改,底層數組不變
The original complex array: [[d e f] [g s i] [j k l]]
問題:
1、complexArray1被傳入函數的話,這個函數中對該參數值的修改會影響到它的原值嗎?
如果修改了引用類型的值會受影響,1.數組的操作不影響原值 2.切片的操作會影響原值。
如果是進行一層修改,即數組的某個完整元素進行修改(指針變化),那么原有數組不變;如果進行二層修改,即數組中某個元素切片內的某個元素再進行修改(指針未改變),那么原有數據也會跟著改變,傳參可以理解是淺copy,參數本身的指針是不同,但是元素指針相同,對元素指針所指向目的的操作會影響傳參過程中的原始數據;
2、函數真正拿到的參數值其實只是它們的副本,那么函數返回給調用方的結果值也會被復制嗎?比如你傳出去一個數組,它還會是函數中的那個數組嗎?
一般來說應該是復制的,傳參和返回應該是一個對稱的過程,本身對這一片內存數據的操作只發生在函數內部,脫離函數就應該脫離這塊內存區域
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。