Go 頭等函式
33. 函式是一等公民(頭等函式)
現在簡單概括一下本教程討論的內容:
- 什麼是頭等函式?
- 匿名函式
- 使用者自定義的函式型別
- 高階函式
- 把函式作為引數,傳遞給其它函式
- 在其它函式中返回函式
- 閉包
- 頭等函式的實際用途
什麼是頭等函式?
支援頭等函式(First Class Function)的程式語言,可以把函式賦值給變數,也可以把函式作為其它函式的引數或者返回值。Go 語言支援頭等函式的機制。
本教程我們會討論頭等函式的語法和用例。
匿名函式
我們來編寫一個簡單的示例,把函式賦值給一個變數。
Copypackage main
import (
"fmt"
)
func main() {
a := func() {
fmt.Println("hello world first class function")
}
a()
fmt.Printf("%T", a)
}
在上面的程式中,我們將一個函式賦值給了變數 a
(第 8 行)。這是把函式賦值給變數的語法。你如果觀察得仔細的話,會發現賦值給 a
的函式沒有名稱。由於沒有名稱,這類函式稱為匿名函式(Anonymous Function)。
呼叫該函式的唯一方法就是使用變數 a
。我們在下一行呼叫了它。a()
呼叫了這個函式,打印出 hello world first class function
a
的型別。這會輸出 func()
。
執行該程式,會輸出:
Copyhello world first class function
func()
要呼叫一個匿名函式,可以不用賦值給變數。通過下面的例子,我們看看這是怎麼做到的。
Copypackage main
import (
"fmt"
)
func main() {
func() {
fmt.Println("hello world first class function")
}()
}
在上面的程式中,第 8 行定義了一個匿名函式,並在定義之後,我們使用 ()
hello world first class function
就像其它函式一樣,還可以向匿名函式傳遞引數。
Copypackage main
import (
"fmt"
)
func main() {
func(n string) {
fmt.Println("Welcome", n)
}("Gophers")
}
在上面的程式中,我們向匿名函式傳遞了一個字串引數(第 10 行)。執行該程式後會輸出:
CopyWelcome Gophers
使用者自定義的函式型別
正如我們定義自己的結構體型別一樣,我們可以定義自己的函式型別。
Copytype add func(a int, b int) int
以上程式碼片段建立了一個新的函式型別 add
,它接收兩個整型引數,並返回一個整型。現在我們來定義 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)
}
在上面程式的第 10 行,我們定義了一個 add
型別的變數 a
,並向它賦值了一個符合 add
型別簽名的函式。我們在第 13 行呼叫了該函式,並將結果賦值給 s
。該程式會輸出:
Sum 11
高階函式
高階函式(Hiher-order Function)定義為:滿足下列條件之一的函式:
- 接收一個或多個函式作為引數
- 返回值是一個函式
針對上述兩種情況,我們看看一些簡單例項。
把函式作為引數,傳遞給其它函式
Copypackage 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)
}
在上面的例項中,第 7 行我們定義了一個函式 simple
,simple
接收一個函式引數(該函式接收兩個 int
引數,返回一個 a
整型)。在 main
函式的第 12 行,我們建立了一個匿名函式 f
,其簽名符合 simple
函式的引數。我們在下一行呼叫了 simple
,並傳遞了引數 f
。該程式列印輸出 67。
在其它函式中返回函式
現在我們重寫上面的程式碼,在 simple
函式中返回一個函式。
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))
}
在上面程式中,第 7 行的 simple
函式返回了一個函式,並接受兩個 int
引數,返回一個 int
。
在第 15 行,我們呼叫了 simple
函式。我們把 simple
的返回值賦值給了 s
。現在 s
包含了 simple
函式返回的函式。我們呼叫了 s
,並向它傳遞了兩個 int 引數(第 16 行)。該程式輸出 67。
閉包
閉包(Closure)是匿名函式的一個特例。當一個匿名函式所訪問的變數定義在函式體的外部時,就稱這樣的匿名函式為閉包。
看看一個示例就明白了。
Copypackage main
import (
"fmt"
)
func main() {
a := 5
func() {
fmt.Println("a =", a)
}()
}
在上面的程式中,匿名函式在第 10 行訪問了變數 a
,而 a
存在於函式體的外部。因此這個匿名函式就是閉包。
每一個閉包都會繫結一個它自己的外圍變數(Surrounding Variable)。我們通過一個簡單示例來體會這句話的含義。
Copypackage 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("!"))
}
在上面程式中,函式 appendStr
返回了一個閉包。這個閉包綁定了變數 t
。我們來理解這是什麼意思。
在第 17 行和第 18 行宣告的變數 a
和 b
都是閉包,它們綁定了各自的 t
值。
我們首先用引數 World
呼叫了 a
。現在 a
中 t
值變為了 Hello World
。
在第 20 行,我們又用引數 Everyone
呼叫了 b
。由於 b
綁定了自己的變數 t
,因此 b
中的 t
還是等於初始值 Hello
。於是該函式呼叫之後,b
中的 t
變為了 Hello Everyone
。程式的其他部分很簡單,不再解釋。
該程式會輸出:
CopyHello World
Hello Everyone
Hello World Gopher
Hello Everyone !
頭等函式的實際用途
迄今為止,我們已經定義了什麼是頭等函式,也看了一些專門設計的示例,來學習它們如何工作。現在我們來編寫一些實際的程式,來展現頭等函式的實際用處。
我們會建立一個程式,基於一些條件,來過濾一個 students
切片。現在我們來逐步實現它。
首先定義一個 student
型別。
type student struct {
firstName string
lastName string
grade string
country string
}
下一步是編寫一個 filter
函式。該函式接收一個 students
切片和一個函式作為引數,這個函式會計算一個學生是否滿足篩選條件。寫出這個函式後,你很快就會明白,我們繼續吧。
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
}
在上面的函式中,filter
的第二個引數是一個函式。這個函式接收 student
引數,返回一個 bool
值。這個函式計算了某一學生是否滿足篩選條件。我們在第 3 行遍歷了 student
切片,將每個學生作為引數傳遞給了函式 f
。如果該函式返回 true
,就表示該學生通過了篩選條件,接著將該學生新增到了結果切片 r
中。你可能會很困惑這個函式的實際用途,等我們完成程式你就知道了。我添加了 main
函式,整個程式如下所示:
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)
}
在 main
函式中,我們首先建立了兩個學生 s1
和 s2
,並將他們新增到了切片 s
。現在假設我們想要查詢所有成績為 B
的學生。為了實現這樣的功能,我們傳遞了一個檢查學生成績是否為 B
的函式,如果是,該函式會返回 true
。我們把這個函式作為引數傳遞給了 filter
函式(第 38 行)。上述程式會輸出:
[{Samuel Johnson B USA}]
假設我們想要查詢所有來自印度的學生。通過修改傳遞給 filter
的函式引數,就很容易地實現了。
實現它的程式碼如下所示:
Copyc := filter(s, func(s student) bool {
if s.country == "India" {
return true
}
return false
})
fmt.Println(c)
請將該函式新增到 main
函式,並檢查它的輸出。
我們最後再編寫一個程式,來結束這一節的討論。這個程式會對切片的每個元素執行相同的操作,並返回結果。例如,如果我們希望將切片中的所有整數乘以 5,並返回出結果,那麼通過頭等函式可以很輕鬆地實現。我們把這種對集合中的每個元素進行操作的函式稱為 map
函式。相關程式碼如下所示,它們很容易看懂。
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)
}
該程式會輸出:
Copy[25 30 35 40 45]