這意味著函數(shù)不但可以用于封裝代碼、分割功能、解耦邏輯,還可以化身為普通的值,在其他函數(shù)間傳遞、賦予變量、做類型判斷和轉(zhuǎn)換等等,就像切片和字典的值那樣。
而更深層次的含義就是:函數(shù)值可以由此成為能夠被隨意傳播的獨(dú)立邏輯組件(或者說功能模塊)。
對于函數(shù)類型來說,它是一種對一組輸入、輸出進(jìn)行模板化的重要工具,它比接口類型更加輕巧、靈活,它的值也借此變成了可被熱替換的邏輯組件。
我先聲明了一個(gè)函數(shù)類型,名叫Printer,注意這里的寫法,在類型聲明的名稱右邊的是func關(guān)鍵字,我們由此就可知道這是一個(gè)函數(shù)類型的聲明。
nc右邊的就是這個(gè)函數(shù)類型的參數(shù)列表和結(jié)果列表。其中,參數(shù)列表必須由圓括號包裹,而只要結(jié)果列表中只有一個(gè)結(jié)果聲明,并且沒有為它命名,我們就可以省略掉外圍的圓括號。
書寫函數(shù)簽名的方式與函數(shù)聲明的是一致的。只是緊挨在參數(shù)列表左邊的不是函數(shù)名稱,而是關(guān)鍵字func。這里函數(shù)名稱和func互換了一下位置而已
函數(shù)的簽名其實(shí)就是函數(shù)的參數(shù)列表和結(jié)果列表的統(tǒng)稱,它定義了可用來鑒別不同函數(shù)的那些特征,同時(shí)也定義了我們與函數(shù)交互的方式。
注意,各個(gè)參數(shù)和結(jié)果的名稱不能算作函數(shù)簽名的一部分,甚至對于結(jié)果聲明來說,沒有名稱都可以。只要兩個(gè)函數(shù)的參數(shù)列表和結(jié)果列表中的元素順序及其類型是一致的,我們就可以說它們是一樣的函數(shù),或者說是實(shí)現(xiàn)了同一個(gè)函數(shù)類型的函數(shù)。嚴(yán)格來說,函數(shù)的名稱也不能算作函數(shù)簽名的一部分,它只是我們在調(diào)用函數(shù)時(shí),需要給定的標(biāo)識符而已。
聲明的函數(shù)printToStd的簽名與Printer的是一致的,因此前者是后者的一個(gè)實(shí)現(xiàn),即使它們的名稱以及有的結(jié)果名稱是不同的。
通過main函數(shù)中的代碼,我們就可以證實(shí)這兩者的關(guān)系了,我順利地把printToStd函數(shù)賦給了Printer類型的變量p,并且成功地調(diào)用了它。
總之,“函數(shù)是一等的公民”是函數(shù)式編程(functional programming)的重要特征。Go 語言在語言層面支持了函數(shù)式編程
package main
import "fmt"
//先聲明了一個(gè)函數(shù)類型,名叫Printer,函數(shù)簽名:函數(shù)的參數(shù)列表和結(jié)果列表的統(tǒng)稱
type Printer func(contents string) (n int, err error)
//定義了一個(gè)函數(shù),printToStd的簽名與Printer的是一致的,因此printToStd是Printer的一個(gè)實(shí)現(xiàn),即使它們的名稱以及有的結(jié)果名稱是不同的
func printToStd(contents string) (bytesNum int, err error) {
return fmt.Println(contents)
}
func main() {
var p Printer //初始化一個(gè)Printer 類型的p
p = printToStd //順利地把printToStd函數(shù)賦給了Printer類型的變量p,并且成功地調(diào)用了它
p("something")
}
go run demo26.go
something
什么是高階函數(shù)?只要滿足了其中任意一個(gè)特點(diǎn),我們就可以說這個(gè)函數(shù)是一個(gè)高階函數(shù)
我想通過編寫calculate函數(shù)來實(shí)現(xiàn)兩個(gè)整數(shù)間的加減乘除運(yùn)算,但是希望兩個(gè)整數(shù)和具體的操作都由該函數(shù)的調(diào)用方給出,那么,這樣一個(gè)函數(shù)應(yīng)該怎樣編寫呢。
我們編寫calculate函數(shù)的簽名部分。這個(gè)函數(shù)除了需要兩個(gè)int類型的參數(shù)之外,還應(yīng)該有一個(gè)operate類型的參數(shù)。該函數(shù)的結(jié)果應(yīng)該有兩個(gè),一個(gè)是int類型的,代表真正的操作結(jié)果,另一個(gè)應(yīng)該是error類型的,因?yàn)槿绻莻€(gè)operate類型的參數(shù)值為nil,那么就應(yīng)該直接返回一個(gè)錯(cuò)誤
函數(shù)類型屬于引用類型,它的值可以為nil,而這種類型的零值恰恰就是nil。
calculate函數(shù)實(shí)現(xiàn)起來就很簡單了。我們需要先用衛(wèi)述語句檢查一下參數(shù),如果operate類型的參數(shù)op為nil,那么就直接返回0和一個(gè)代表了具體錯(cuò)誤的error類型值。
衛(wèi)述語句是指被用來檢查關(guān)鍵的先決條件的合法性,并在檢查未通過的情況下立即終止當(dāng)前代碼塊執(zhí)行的語句。在 Go 語言中,if 語句常被作為衛(wèi)述語句。如果檢查無誤,那么就調(diào)用op并把那兩個(gè)操作數(shù)傳給它,最后返回op返回的結(jié)果和代表沒有錯(cuò)誤發(fā)生的nil。
calculate函數(shù)的其中一個(gè)參數(shù)是operate類型的,而且后者就是一個(gè)函數(shù)類型。在調(diào)用calculate函數(shù)的時(shí)候,我們需要傳入一個(gè)operate類型的函數(shù)值。這個(gè)函數(shù)值應(yīng)該怎么寫?
只要它的簽名與operate類型的簽名一致,并且實(shí)現(xiàn)得當(dāng)就可以了。我們可以像上一個(gè)例子那樣先聲明好一個(gè)函數(shù),再把它賦給一個(gè)變量,也可以直接編寫一個(gè)實(shí)現(xiàn)了operate類型的匿名函數(shù)。
calculate函數(shù)就是一個(gè)高階函數(shù)。但是我們說高階函數(shù)的特點(diǎn)有兩個(gè),而該函數(shù)只展示了其中一個(gè)特點(diǎn),即:接受其他的函數(shù)作為參數(shù)傳入。
那另一個(gè)特點(diǎn),把其他的函數(shù)作為結(jié)果返回。這又是怎么玩的呢?你可以看看我在 demo27.go 文件中聲明的函數(shù)類型calculateFunc和函數(shù)genCalculator。其中,genCalculator函數(shù)的唯一結(jié)果的類型就是calculateFunc
package main
import (
"errors"
"fmt"
)
type operate func(x, y int) int //我們來聲明一個(gè)名叫operate的函數(shù)類型,它有兩個(gè)參數(shù)和一個(gè)結(jié)果,都是int類型的。
// 方案1。calculate函數(shù)就是一個(gè)高階函數(shù)。該函數(shù)只展示了其中一個(gè)特點(diǎn),即:接受其他的函數(shù)作為參數(shù)傳入。
func calculate(x int, y int, op operate) (int, error) {
if op == nil { //衛(wèi)述語句檢查op的合法性
return 0, errors.New("invalid operation")
}
return op(x, y), nil
}
// 方案2。calculateFunc也是高階函數(shù),把其他的函數(shù)op作為結(jié)果返回
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) //把函數(shù)op作為一個(gè)普通的值賦給一個(gè)變量。
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>)
閉包又是什么?你可以想象一下,在一個(gè)函數(shù)中存在對外來標(biāo)識符的引用。所謂的外來標(biāo)識符,既不代表當(dāng)前函數(shù)的任何參數(shù)或結(jié)果,也不是函數(shù)內(nèi)部聲明的,它是直接從外邊拿過來的。
還有個(gè)專門的術(shù)語稱呼它,叫自由變量,可見它代表的肯定是個(gè)變量。實(shí)際上,如果它是個(gè)常量,那也就形成不了閉包了,因?yàn)槌A渴遣豢勺兊某绦驅(qū)嶓w,而閉包體現(xiàn)的卻是由“不確定”變?yōu)椤按_定”的一個(gè)過程。
我們說的這個(gè)函數(shù)(以下簡稱閉包函數(shù))就是因?yàn)橐昧俗杂勺兞?,而呈現(xiàn)出了一種“不確定”的狀態(tài),也叫“開放”狀態(tài)。
也就是說,它的內(nèi)部邏輯并不是完整的,有一部分邏輯需要這個(gè)自由變量參與完成,而后者到底代表了什么在閉包函數(shù)被定義的時(shí)候卻是未知的。
即使對于像 Go 語言這種靜態(tài)類型的編程語言而言,我們在定義閉包函數(shù)的時(shí)候最多也只能知道自由變量的類型
在我們剛剛提到的genCalculator函數(shù)內(nèi)部,實(shí)際上就實(shí)現(xiàn)了一個(gè)閉包,而genCalculator函數(shù)也是一個(gè)高階函數(shù)。
genCalculator函數(shù)只做了一件事,那就是定義一個(gè)匿名的、calculateFunc類型的函數(shù)并把它作為結(jié)果值返回。
而這個(gè)匿名的函數(shù)就是一個(gè)閉包函數(shù)。它里面使用的變量op既不代表它的任何參數(shù)或結(jié)果也不是它自己聲明的,而是定義它的genCalculator函數(shù)的參數(shù),所以是一個(gè)自由變量。
這個(gè)自由變量究竟代表了什么,這一點(diǎn)并不是在定義這個(gè)閉包函數(shù)的時(shí)候確定的,而是在genCalculator函數(shù)被調(diào)用的時(shí)候確定的。只有給定了該函數(shù)的參數(shù)op,我們才能知道它返回給我們的閉包函數(shù)可以用于什么運(yùn)算。
看到if op == nil {那一行了嗎?Go 語言編譯器讀到這里時(shí)會(huì)試圖去尋找op所代表的東西,它會(huì)發(fā)現(xiàn)op代表的是genCalculator函數(shù)的參數(shù),然后,它會(huì)把這兩者聯(lián)系起來。這時(shí)可以說,自由變量op被“捕獲”了。
當(dāng)程序運(yùn)行到這里的時(shí)候,op就是那個(gè)參數(shù)值了。如此一來,這個(gè)閉包函數(shù)的狀態(tài)就由“不確定”變?yōu)榱恕按_定”,或者說轉(zhuǎn)到了“閉合”狀態(tài),至此也就真正地形成了一個(gè)閉包。
看出來了嗎?我們在用高階函數(shù)實(shí)現(xiàn)閉包。這也是高階函數(shù)的一大功用。
(高階函數(shù)與閉包)
那么,實(shí)現(xiàn)閉包的意義又在哪里呢?表面上看,我們只是延遲實(shí)現(xiàn)了一部分程序邏輯或功能而已,但實(shí)際上,我們是在動(dòng)態(tài)地生成那部分程序邏輯。
我們可以借此在程序運(yùn)行的過程中,根據(jù)需要生成功能不同的函數(shù),繼而影響后續(xù)的程序行為。這與 GoF 設(shè)計(jì)模式中的“模板方法”模式有著異曲同工之妙,不是嗎?
這個(gè)命令源碼文件(也就是 demo28.go示例一)在運(yùn)行之后會(huì)輸出什么?
答案是:原數(shù)組不會(huì)改變。為什么呢?原因是,所有傳給函數(shù)的參數(shù)值都會(huì)被復(fù)制,函數(shù)在其內(nèi)部使用的并不是參數(shù)值的原值,而是它的副本。由于數(shù)組是值類型,所以每一次復(fù)制都會(huì)拷貝它,以及它的所有元素值。我在modify函數(shù)中修改的只是原數(shù)組的副本而已,并不會(huì)對原數(shù)組造成任何影響。
對于引用類型,比如:切片、字典、通道,像上面那樣復(fù)制它們的值,只會(huì)拷貝它們本身而已,并不會(huì)拷貝它們引用的底層數(shù)據(jù)。也就是說,這時(shí)只是淺表復(fù)制,而不是深層復(fù)制。
以切片值為例,如此復(fù)制的時(shí)候,只是拷貝了它指向底層數(shù)組中某一個(gè)元素的指針,以及它的長度值和容量值,而它的底層數(shù)組并不會(huì)被拷貝。
另外還要注意,就算我們傳入函數(shù)的是一個(gè)值類型的參數(shù)值,但如果這個(gè)參數(shù)值中的某個(gè)元素是引用類型的,那么我們?nèi)匀灰⌒摹?/p>
變量complexArray1是[3][]string類型的,也就是說,雖然它是一個(gè)數(shù)組,但是其中的每個(gè)元素又都是一個(gè)切片。這樣一個(gè)值被傳入函數(shù)的話,函數(shù)中對該參數(shù)值的修改會(huì)影響到complexArray1本身嗎?我想,這可以留作今天的思考題。
package main
import "fmt"
func main() {
// 示例1。底層數(shù)組不會(huì)被修改,所有傳給函數(shù)的參數(shù)值都會(huì)被復(fù)制,函數(shù)在其內(nèi)部使用的并不是參數(shù)值的原值,而是它的副本
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。切片會(huì)被修改掉,但是切片底層的數(shù)組不變
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。 /切片被修改,底層數(shù)組不變
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) //切片被修改,底層數(shù)組不變
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]] //切片被修改,底層數(shù)組不變
The original complex array: [[d e f] [g s i] [j k l]]
問題:
1、complexArray1被傳入函數(shù)的話,這個(gè)函數(shù)中對該參數(shù)值的修改會(huì)影響到它的原值嗎?
如果修改了引用類型的值會(huì)受影響,1.數(shù)組的操作不影響原值 2.切片的操作會(huì)影響原值。
如果是進(jìn)行一層修改,即數(shù)組的某個(gè)完整元素進(jìn)行修改(指針變化),那么原有數(shù)組不變;如果進(jìn)行二層修改,即數(shù)組中某個(gè)元素切片內(nèi)的某個(gè)元素再進(jìn)行修改(指針未改變),那么原有數(shù)據(jù)也會(huì)跟著改變,傳參可以理解是淺copy,參數(shù)本身的指針是不同,但是元素指針相同,對元素指針?biāo)赶蚰康牡牟僮鲿?huì)影響傳參過程中的原始數(shù)據(jù);
2、函數(shù)真正拿到的參數(shù)值其實(shí)只是它們的副本,那么函數(shù)返回給調(diào)用方的結(jié)果值也會(huì)被復(fù)制嗎?比如你傳出去一個(gè)數(shù)組,它還會(huì)是函數(shù)中的那個(gè)數(shù)組嗎?
一般來說應(yīng)該是復(fù)制的,傳參和返回應(yīng)該是一個(gè)對稱的過程,本身對這一片內(nèi)存數(shù)據(jù)的操作只發(fā)生在函數(shù)內(nèi)部,脫離函數(shù)就應(yīng)該脫離這塊內(nèi)存區(qū)域
另外有需要云服務(wù)器可以了解下創(chuàng)新互聯(lián)cdcxhl.cn,海內(nèi)外云服務(wù)器15元起步,三天無理由+7*72小時(shí)售后在線,公司持有idc許可證,提供“云服務(wù)器、裸金屬服務(wù)器、高防服務(wù)器、香港服務(wù)器、美國服務(wù)器、虛擬主機(jī)、免備案服務(wù)器”等云主機(jī)租用服務(wù)以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡單易用、服務(wù)可用性高、性價(jià)比高”等特點(diǎn)與優(yōu)勢,專為企業(yè)上云打造定制,能夠滿足用戶豐富、多元化的應(yīng)用場景需求。
分享標(biāo)題:go函數(shù)使用,閉包-創(chuàng)新互聯(lián)
當(dāng)前網(wǎng)址:http://www.rwnh.cn/article6/cegoig.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)頁設(shè)計(jì)公司、網(wǎng)站導(dǎo)航、動(dòng)態(tài)網(wǎng)站、手機(jī)網(wǎng)站建設(shè)、標(biāo)簽優(yōu)化、做網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)
猜你還喜歡下面的內(nèi)容