?Go 經(jīng)典入門系列 33:函數(shù)是一等公民(頭等函數(shù))

歡迎來到 Golang 系列教程[1]的第 33 篇。
什么是頭等函數(shù)?
支持頭等函數(shù)(First Class Function)的編程語言,可以把函數(shù)賦值給變量,也可以把函數(shù)作為其它函數(shù)的參數(shù)或者返回值。Go 語言支持頭等函數(shù)的機(jī)制。
本教程我們會討論頭等函數(shù)的語法和用例。
匿名函數(shù)
我們來編寫一個簡單的示例,把函數(shù)[2]賦值給一個變量[3]。
package?main
import?(
????"fmt"
)
func?main()?{
????a?:=?func()?{
????????fmt.Println("hello?world?first?class?function")
????}
????a()
????fmt.Printf("%T",?a)
}
在 playground 上運行[4]
在上面的程序中,我們將一個函數(shù)賦值給了變量 a(第 8 行)。這是把函數(shù)賦值給變量的語法。你如果觀察得仔細(xì)的話,會發(fā)現(xiàn)賦值給 a 的函數(shù)沒有名稱。由于沒有名稱,這類函數(shù)稱為匿名函數(shù)(Anonymous Function)。
調(diào)用該函數(shù)的唯一方法就是使用變量 a。我們在下一行調(diào)用了它。a() 調(diào)用了這個函數(shù),打印出 hello world first class function。在第 12 行,我們打印出 a 的類型。這會輸出 func()。
運行該程序,會輸出:
hello?world?first?class?function
func()
要調(diào)用一個匿名函數(shù),可以不用賦值給變量。通過下面的例子,我們看看這是怎么做到的。
package?main
import?(
????"fmt"
)
func?main()?{
????func()?{
????????fmt.Println("hello?world?first?class?function")
????}()
}
在 playground 上運行[5]
在上面的程序中,第 8 行定義了一個匿名函數(shù),并在定義之后,我們使用 () 立即調(diào)用了該函數(shù)(第 10 行)。該程序會輸出:
hello?world?first?class?function
就像其它函數(shù)一樣,還可以向匿名函數(shù)傳遞參數(shù)。
package?main
import?(
????"fmt"
)
func?main()?{
????func(n?string)?{
????????fmt.Println("Welcome",?n)
????}("Gophers")
}
在 playground 上運行[6]
在上面的程序中,我們向匿名函數(shù)傳遞了一個字符串參數(shù)(第 10 行)。運行該程序后會輸出:
Welcome?Gophers
用戶自定義的函數(shù)類型
正如我們定義自己的結(jié)構(gòu)體[7]類型一樣,我們可以定義自己的函數(shù)類型。
type?add?func(a?int,?b?int)?int
以上代碼片段創(chuàng)建了一個新的函數(shù)類型 add,它接收兩個整型參數(shù),并返回一個整型。現(xiàn)在我們來定義 add 類型的變量。
我們來編寫一個程序,定義一個 add 類型的變量。
package?main
import?(
????"fmt"
)
type?add?func(a?int,?b?int)?int
func?main()?{
????var?a?add?=?func(a?int,?b?int)?int?{
????????return?a?+?b
????}
????s?:=?a(5,?6)
????fmt.Println("Sum",?s)
}
在 playground 上運行[8]
在上面程序的第 10 行,我們定義了一個 add 類型的變量 a,并向它賦值了一個符合 add 類型簽名的函數(shù)。我們在第 13 行調(diào)用了該函數(shù),并將結(jié)果賦值給 s。該程序會輸出:
Sum?11
高階函數(shù)
wiki[9] 把高階函數(shù)(Hiher-order Function)定義為:滿足下列條件之一的函數(shù):
接收一個或多個函數(shù)作為參數(shù) 返回值是一個函數(shù)
針對上述兩種情況,我們看看一些簡單實例。
把函數(shù)作為參數(shù),傳遞給其它函數(shù)
package?main
import?(
????"fmt"
)
func?simple(a?func(a,?b?int)?int)?{
????fmt.Println(a(60,?7))
}
func?main()?{
????f?:=?func(a,?b?int)?int?{
????????return?a?+?b
????}
????simple(f)
}
在 playground 上運行[10]
在上面的實例中,第 7 行我們定義了一個函數(shù) simple,simple 接收一個函數(shù)參數(shù)(該函數(shù)接收兩個 int 參數(shù),返回一個 a 整型)。在 main 函數(shù)的第 12 行,我們創(chuàng)建了一個匿名函數(shù) f,其簽名符合 simple 函數(shù)的參數(shù)。我們在下一行調(diào)用了 simple,并傳遞了參數(shù) f。該程序打印輸出 67。
在其它函數(shù)中返回函數(shù)
現(xiàn)在我們重寫上面的代碼,在 simple 函數(shù)中返回一個函數(shù)。
package?main
import?(
????"fmt"
)
func?simple()?func(a,?b?int)?int?{
????f?:=?func(a,?b?int)?int?{
????????return?a?+?b
????}
????return?f
}
func?main()?{
????s?:=?simple()
????fmt.Println(s(60,?7))
}
在 playground 上運行[11]
在上面程序中,第 7 行的 simple 函數(shù)返回了一個函數(shù),并接受兩個 int 參數(shù),返回一個 int。
在第 15 行,我們調(diào)用了 simple 函數(shù)。我們把 simple 的返回值賦值給了 s。現(xiàn)在 s 包含了 simple 函數(shù)返回的函數(shù)。我們調(diào)用了 s,并向它傳遞了兩個 int 參數(shù)(第 16 行)。該程序輸出 67。
閉包
閉包(Closure)是匿名函數(shù)的一個特例。當(dāng)一個匿名函數(shù)所訪問的變量定義在函數(shù)體的外部時,就稱這樣的匿名函數(shù)為閉包。
看看一個示例就明白了。
package?main
import?(
????"fmt"
)
func?main()?{
????a?:=?5
????func()?{
????????fmt.Println("a?=",?a)
????}()
}
在 playground 上運行[12]
在上面的程序中,匿名函數(shù)在第 10 行訪問了變量 a,而 a 存在于函數(shù)體的外部。因此這個匿名函數(shù)就是閉包。
每一個閉包都會綁定一個它自己的外圍變量(Surrounding Variable)。我們通過一個簡單示例來體會這句話的含義。
package?main
import?(
????"fmt"
)
func?appendStr()?func(string)?string?{
????t?:=?"Hello"
????c?:=?func(b?string)?string?{
????????t?=?t?+?"?"?+?b
????????return?t
????}
????return?c
}
func?main()?{
????a?:=?appendStr()
????b?:=?appendStr()
????fmt.Println(a("World"))
????fmt.Println(b("Everyone"))
????fmt.Println(a("Gopher"))
????fmt.Println(b("!"))
}
在 playground 上運行[13]
在上面程序中,函數(shù) appendStr 返回了一個閉包。這個閉包綁定了變量 t。我們來理解這是什么意思。
在第 17 行和第 18 行聲明的變量 a 和 b 都是閉包,它們綁定了各自的 t 值。
我們首先用參數(shù) World 調(diào)用了 a。現(xiàn)在 a 中 t 值變?yōu)榱?Hello World。
在第 20 行,我們又用參數(shù) Everyone 調(diào)用了 b。由于 b 綁定了自己的變量 t,因此 b 中的 t 還是等于初始值 Hello。于是該函數(shù)調(diào)用之后,b 中的 t 變?yōu)榱?Hello Everyone。程序的其他部分很簡單,不再解釋。
該程序會輸出:
Hello?World
Hello?Everyone
Hello?World?Gopher
Hello?Everyone?!
頭等函數(shù)的實際用途
迄今為止,我們已經(jīng)定義了什么是頭等函數(shù),也看了一些專門設(shè)計的示例,來學(xué)習(xí)它們?nèi)绾喂ぷ鳌,F(xiàn)在我們來編寫一些實際的程序,來展現(xiàn)頭等函數(shù)的實際用處。
我們會創(chuàng)建一個程序,基于一些條件,來過濾一個 students 切片。現(xiàn)在我們來逐步實現(xiàn)它。
首先定義一個 student 類型。
type?student?struct?{
????firstName?string
????lastName?string
????grade?string
????country?string
}
下一步是編寫一個 filter 函數(shù)。該函數(shù)接收一個 students 切片和一個函數(shù)作為參數(shù),這個函數(shù)會計算一個學(xué)生是否滿足篩選條件。寫出這個函數(shù)后,你很快就會明白,我們繼續(xù)吧。
func?filter(s?[]student,?f?func(student)?bool)?[]student?{
????var?r?[]student
????for?_,?v?:=?range?s?{
????????if?f(v)?==?true?{
????????????r?=?append(r,?v)
????????}
????}
????return?r
}
在上面的函數(shù)中,filter 的第二個參數(shù)是一個函數(shù)。這個函數(shù)接收 student 參數(shù),返回一個 bool 值。這個函數(shù)計算了某一學(xué)生是否滿足篩選條件。我們在第 3 行遍歷了 student 切片,將每個學(xué)生作為參數(shù)傳遞給了函數(shù) f。如果該函數(shù)返回 true,就表示該學(xué)生通過了篩選條件,接著將該學(xué)生添加到了結(jié)果切片 r 中。你可能會很困惑這個函數(shù)的實際用途,等我們完成程序你就知道了。我添加了 main 函數(shù),整個程序如下所示:
package?main
import?(
????"fmt"
)
type?student?struct?{
????firstName?string
????lastName??string
????grade?????string
????country???string
}
func?filter(s?[]student,?f?func(student)?bool)?[]student?{
????var?r?[]student
????for?_,?v?:=?range?s?{
????????if?f(v)?==?true?{
????????????r?=?append(r,?v)
????????}
????}
????return?r
}
func?main()?{
????s1?:=?student{
????????firstName:?"Naveen",
????????lastName:??"Ramanathan",
????????grade:?????"A",
????????country:???"India",
????}
????s2?:=?student{
????????firstName:?"Samuel",
????????lastName:??"Johnson",
????????grade:?????"B",
????????country:???"USA",
????}
????s?:=?[]student{s1,?s2}
????f?:=?filter(s,?func(s?student)?bool?{
????????if?s.grade?==?"B"?{
????????????return?true
????????}
????????return?false
????})
????fmt.Println(f)
}
在 playground 上運行[14]
在 main 函數(shù)中,我們首先創(chuàng)建了兩個學(xué)生 s1 和 s2,并將他們添加到了切片 s。現(xiàn)在假設(shè)我們想要查詢所有成績?yōu)?B 的學(xué)生。為了實現(xiàn)這樣的功能,我們傳遞了一個檢查學(xué)生成績是否為 B 的函數(shù),如果是,該函數(shù)會返回 true。我們把這個函數(shù)作為參數(shù)傳遞給了 filter 函數(shù)(第 38 行)。上述程序會輸出:
[{Samuel?Johnson?B?USA}]
假設(shè)我們想要查找所有來自印度的學(xué)生。通過修改傳遞給 filter 的函數(shù)參數(shù),就很容易地實現(xiàn)了。
實現(xiàn)它的代碼如下所示:
c?:=?filter(s,?func(s?student)?bool?{
????if?s.country?==?"India"?{
????????return?true
????}
????return?false
})
fmt.Println(c)
請將該函數(shù)添加到 main 函數(shù),并檢查它的輸出。
我們最后再編寫一個程序,來結(jié)束這一節(jié)的討論。這個程序會對切片的每個元素執(zhí)行相同的操作,并返回結(jié)果。例如,如果我們希望將切片中的所有整數(shù)乘以 5,并返回出結(jié)果,那么通過頭等函數(shù)可以很輕松地實現(xiàn)。我們把這種對集合中的每個元素進(jìn)行操作的函數(shù)稱為 map 函數(shù)。相關(guān)代碼如下所示,它們很容易看懂。
package?main
import?(
????"fmt"
)
func?iMap(s?[]int,?f?func(int)?int)?[]int?{
????var?r?[]int
????for?_,?v?:=?range?s?{
????????r?=?append(r,?f(v))
????}
????return?r
}
func?main()?{
????a?:=?[]int{5,?6,?7,?8,?9}
????r?:=?iMap(a,?func(n?int)?int?{
????????return?n?*?5
????})
????fmt.Println(r)
}
在 playground 上運行[15]
該程序會輸出:
[25?30?35?40?45]
現(xiàn)在簡單概括一下本教程討論的內(nèi)容:
什么是頭等函數(shù)? 匿名函數(shù) 用戶自定義的函數(shù)類型 高階函數(shù) 把函數(shù)作為參數(shù),傳遞給其它函數(shù) 在其它函數(shù)中返回函數(shù) 閉包 頭等函數(shù)的實際用途
本教程到此結(jié)束。祝你愉快。
上一教程 - panic 和 recover
下一教程 - 反射[16]
via: https://golangbot.com/first-class-functions/
作者:Nick Coghlan[17]譯者:Noluye[18]校對:polaris1119[19]
本文由 GCTT[20] 原創(chuàng)編譯,Go 中文網(wǎng)[21] 榮譽(yù)推出
參考資料
Golang 系列教程: https://studygolang.com/subject/2
[2]函數(shù): https://studygolang.com/articles/11892
[3]變量: https://studygolang.com/articles/11756
[4]在 playground 上運行: https://play.golang.org/p/Xm_ihamhlEv
[5]在 playground 上運行: https://play.golang.org/p/c0AjB3g8UEn
[6]在 playground 上運行: https://play.golang.org/p/9ttJ5Wi4fj4
[7]結(jié)構(gòu)體: https://studygolang.com/articles/12263
[8]在 playground 上運行: https://play.golang.org/p/n3yPQ7hG7ip
[9]wiki: https://en.wikipedia.org/wiki/Higher-order_function
[10]在 playground 上運行: https://play.golang.org/p/C0MNwz2TSGU
[11]在 playground 上運行: https://play.golang.org/p/82y2caejUy8
[12]在 playground 上運行: https://play.golang.org/p/6QriMs-zbnf
[13]在 playground 上運行: https://play.golang.org/p/134NiQGPOcS
[14]在 playground 上運行: https://play.golang.org/p/YUL1CqSrvfc
[15]在 playground 上運行: https://play.golang.org/p/cs37QwCQ_0H
[16]反射: https://studygolang.com/articles/13178
[17]Nick Coghlan: https://golangbot.com/about/
[18]Noluye: https://github.com/Noluye
[19]polaris1119: https://github.com/polaris1119
[20]GCTT: https://github.com/studygolang/GCTT
[21]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀
