歡迎來到 Golang 系列教程[1]的第 33 篇。
什麼是頭等函數?支持頭等函數(First Class Function)的程式語言,可以把函數賦值給變量,也可以把函數作為其它函數的參數或者返回值。Go 語言支持頭等函數的機制。
本教程我們會討論頭等函數的語法和用例。
匿名函數我們來編寫一個簡單的示例,把函數[2]賦值給一個變量[3]。
package main
import (
"fmt"
)
func main() {
a := func() {
fmt.Println("hello world first class function")
}
a()
fmt.Printf("%T", a)
}在 playground 上運行[4]
在上面的程序中,我們將一個函數賦值給了變量 a(第 8 行)。這是把函數賦值給變量的語法。你如果觀察得仔細的話,會發現賦值給 a 的函數沒有名稱。由於沒有名稱,這類函數稱為匿名函數(Anonymous Function)。
調用該函數的唯一方法就是使用變量 a。我們在下一行調用了它。a() 調用了這個函數,列印出 hello world first class function。在第 12 行,我們列印出 a 的類型。這會輸出 func()。
運行該程序,會輸出:
hello world first class function
func()要調用一個匿名函數,可以不用賦值給變量。通過下面的例子,我們看看這是怎麼做到的。
package main
import (
"fmt"
)
func main() {
func() {
fmt.Println("hello world first class function")
}()
}在 playground 上運行[5]
在上面的程序中,第 8 行定義了一個匿名函數,並在定義之後,我們使用 () 立即調用了該函數(第 10 行)。該程序會輸出:
hello world first class function就像其它函數一樣,還可以向匿名函數傳遞參數。
package main
import (
"fmt"
)
func main() {
func(n string) {
fmt.Println("Welcome", n)
}("Gophers")
}在 playground 上運行[6]
在上面的程序中,我們向匿名函數傳遞了一個字符串參數(第 10 行)。運行該程序後會輸出:
Welcome Gophers
用戶自定義的函數類型正如我們定義自己的結構體[7]類型一樣,我們可以定義自己的函數類型。
type 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)
}在 playground 上運行[8]
在上面程序的第 10 行,我們定義了一個 add 類型的變量 a,並向它賦值了一個符合 add 類型籤名的函數。我們在第 13 行調用了該函數,並將結果賦值給 s。該程序會輸出:
Sum 11
高階函數wiki[9] 把高階函數(Hiher-order Function)定義為:滿足下列條件之一的函數:
針對上述兩種情況,我們看看一些簡單實例。
把函數作為參數,傳遞給其它函數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 行我們定義了一個函數 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))
}在 playground 上運行[11]
在上面程序中,第 7 行的 simple 函數返回了一個函數,並接受兩個 int 參數,返回一個 int。
在第 15 行,我們調用了 simple 函數。我們把 simple 的返回值賦值給了 s。現在 s 包含了 simple 函數返回的函數。我們調用了 s,並向它傳遞了兩個 int 參數(第 16 行)。該程序輸出 67。
閉包閉包(Closure)是匿名函數的一個特例。當一個匿名函數所訪問的變量定義在函數體的外部時,就稱這樣的匿名函數為閉包。
看看一個示例就明白了。
package main
import (
"fmt"
)
func main() {
a := 5
func() {
fmt.Println("a =", a)
}()
}在 playground 上運行[12]
在上面的程序中,匿名函數在第 10 行訪問了變量 a,而 a 存在於函數體的外部。因此這個匿名函數就是閉包。
每一個閉包都會綁定一個它自己的外圍變量(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]
在上面程序中,函數 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。程序的其他部分很簡單,不再解釋。
該程序會輸出:
Hello 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)
}在 playground 上運行[14]
在 main 函數中,我們首先創建了兩個學生 s1 和 s2,並將他們添加到了切片 s。現在假設我們想要查詢所有成績為 B 的學生。為了實現這樣的功能,我們傳遞了一個檢查學生成績是否為 B 的函數,如果是,該函數會返回 true。我們把這個函數作為參數傳遞給了 filter 函數(第 38 行)。上述程序會輸出:
[{Samuel Johnson B USA}]假設我們想要查找所有來自印度的學生。通過修改傳遞給 filter 的函數參數,就很容易地實現了。
實現它的代碼如下所示:
c := 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)
}在 playground 上運行[15]
該程序會輸出:
[25 30 35 40 45]現在簡單概括一下本教程討論的內容:
本教程到此結束。祝你愉快。
上一教程 - panic 和 recover
下一教程 - 反射[16]
via: https://golangbot.com/first-class-functions/
作者:Nick Coghlan[17]譯者:Noluye[18]校對:polaris1119[19]
本文由 GCTT[20] 原創編譯,Go 中文網[21] 榮譽推出
參考資料[1]Golang 系列教程: https://studygolang.com/subject/2
[2]函數: 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]結構體: 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 中文網: https://studygolang.com/