在 Go 中,函式是一級值。 這代表你可以對函式做和其他值一樣的事:把函式指定給變數、把它們當作引數傳給其他函式,甚至可以從其他函式回傳函式。
下面我們建立了兩個函式,engGreeting和espGreeting,並將它們指定給變數greeting:
import "fmt"
func engGreeting(name string) string {
return fmt.Sprintf("Hello %s, nice to meet you!", name)
}
func espGreeting(name string) string {
return fmt.Sprintf("¡Hola %s, mucho gusto!", name)
}
greeting := engGreeting // greeting is a variable of type func(string) string
fmt.Println(greeting("Alice")) // Hello Alice, nice to meet you!
greeting = espGreeting
fmt.Println(greeting("Alice")) // ¡Hola Alice, mucho gusto!
函式值讓我們有機會用行為來參數化函式,而不只是用資料。
在下面的例子中,我們透過greetingFunc參數把行為傳入dialog函式:
func dialog(name string, greetingFunc func(string) string) {
fmt.Println(greetingFunc(name))
fmt.Println("I'm a dialog bot.")
}
func espGreeting(name string) string {
return fmt.Sprintf("¡Hola %s, mucho gusto!", name)
}
greeting := espGreeting
dialog("Alice", greeting)
// =>
// ¡Hola Alice, mucho gusto!
// I'm a dialog bot.
未初始化的函式型別變數,其值是nil。
因此,呼叫nil的函式值會導致 panic。
var dutchGreeting func(string) string
dutchGreeting("Alice") // panic: call of nil function
函式值可以與nil比較。
這有助於避免程式發生不必要的 panic。
var dutchGreeting func(string) string
if dutchGreeting != nil {
dutchGreeting("Alice") // safe to call dutchGreeting
}
之所以能使用函式值,都要歸功於 Go 的函式型別。
函式型別代表一組函式,這些函式具有相同的參數型別序列和相同的結果型別序列。
使用者自訂型別可以宣告在函式型別之上。
舉例來說,前面例子中的dialog函式可以改寫如下:
type greetingFunc func(string) string
func dialog(name string, f greetingFunc) {
fmt.Println(f(name))
fmt.Println("I'm a dialog bot.")
}
因為支援一級函式,我們還能使用另一個強大的工具:匿名函式。
匿名函式在使用的地方定義,func關鍵字後面不接名稱。
這類函式可以存取外層函式的變數。
例如:
func fib() func() int {
var n1, n2 int
return func() int {
if n1 == 0 && n2 == 0 {
n1 = 1
} else {
n1, n2 = n2, n1 + n2
}
return n2
}
}
next := fib()
for i := range N {
fmt.Printf("F%d\t= %4d\n", i, next())
}
呼叫fib會宣告變數n1和n2,並回傳一個匿名函式;這個匿名函式每次被呼叫時,會改變這些變數的值。
匿名函式第 N 次被呼叫時,會回傳費氏數列中從 0 開始的第 N 個數字。內部的匿名函式可以存取外層函式fib的區域變數(n1和n2)。
這是讓函式值在多次呼叫之間保留狀態的好方法。
我們說這個匿名函式是變數n1和n2的閉包。閉包在程式設計中被廣泛使用,你可能也會看到其他語言支援它們。
Bob 是一位財務顧問,協助人們管理支出。 Bob 的客戶會把支出記錄寄給他分析。 Bob 手上有先前期間的記錄,因此能看出花費的變化。 Bob 不喜歡行事曆,所以改用 Bob epoch 來取代日期。 Bob epoch 是從 Bob 的客戶開始活動起算,已經過的天數。
在這個練習中,你將打造一支程式,協助 Bob 管理和分析客戶的支出。
Bob 處理的是Record和DaysPeriod。
一筆Record代表一筆支出記錄,其中包含這筆支出發生的日期、花費的金額,以及這筆支出的類別。
// Record represents an expense record.
type Record struct {
Day int
Amount float64
Category string
}
一個DaysPeriod代表一段日期範圍,涵蓋從 From 這天到 To 這天的所有日期。
範圍的兩端都包含在內。
// DaysPeriod represents a period of days.
type DaysPeriod struct {
From int
To int
}
p := DaysPeriod{From: 1, To: 31}
// p represents all days from the day 1 to the day 31:
// - days 1, 20, 16 and 31 are examples of days that are included
// in the range of time specified by p
// - days 50 and 40 are examples of days that are not included
// in the range of time specified by p
Bob 每天都要處理大量記錄,但根據他正在做的分析,並不是每一筆都有意義。 我們來幫 Bob 對記錄做一些基本的篩選吧。
實作泛型的Filter函式,依照某個函式給定的條件來篩選記錄。
這個篩選函式接受一組記錄和一個述語函式,只回傳集合中符合述語的那些記錄。
records := []Record{
{Day: 1, Amount: 15, Category: "groceries"},
{Day: 11, Amount: 300, Category: "utility-bills"},
{Day: 12, Amount: 28, Category: "groceries"},
}
// Day1Records only returns true for records that are from day 1
func Day1Records(r Record) bool {
return r.Day == 1
}
Filter(records, Day1Records)
// =>
// [
// {Day: 1, Amount: 15, Category: "groceries"}
// ]
Bob 經常需要篩選落在特定期間內的記錄。
實作ByDaysPeriod函式,幫助 Bob 建立這類篩選器。
這個函式接受一個DaysPeriod,並回傳一個函式;該函式接受一筆記錄,並判斷這筆記錄是否落在引數所給的DaysPeriod指定的期間內。
records := []Record{
{Day: 1, Amount: 15, Category: "groceries"},
{Day: 11, Amount: 300, Category: "utility-bills"},
{Day: 12, Amount: 28, Category: "groceries"},
{Day: 26, Amount: 300, Category: "university"},
{Day: 28, Amount: 1300, Category: "rent"},
}
period := DaysPeriod{From: 1, To: 15}
Filter(records, ByDaysPeriod(period))
// =>
// [
// {Day: 1, Amount: 15, Category: "groceries"},
// {Day: 11, Amount: 300, Category: "utility-bills"},
// {Day: 12, Amount: 28, Category: "groceries"},
// ]
除了依期間篩選記錄,Bob 也需要依類別篩選記錄。
實作ByCategory函式,幫助 Bob 建立這類篩選器。
這個函式接受一個類別,並回傳一個函式;該函式接受一筆記錄,並判斷這筆記錄的類別是否和引數所給的類別相同。
records := []Record{
{Day: 1, Amount: 15, Category: "groceries"},
{Day: 11, Amount: 300, Category: "utility-bills"},
{Day: 12, Amount: 28, Category: "groceries"},
{Day: 28, Amount: 1300, Category: "rent"},
}
Filter(records, ByCategory("groceries"))
// =>
// [
// {Day: 1, Amount: 15, Category: "groceries"},
// {Day: 12, Amount: 28, Category: "groceries"},
// ]
Bob 也想知道某段期間內的支出總額。
實作TotalByPeriod函式,回傳該日期期間內的支出總和。
records := []Record{
{Day: 15, Amount: 16, Category: "entertainment"},
{Day: 32, Amount: 20, Category: "groceries"},
{Day: 40, Amount: 30, Category: "entertainment"}
}
p1 := DaysPeriod{From: 1, To: 30}
p2 := DaysPeriod{From: 31, To: 60}
TotalByPeriod(records, p1)
// => 16
TotalByPeriod(records, p2)
// => 50
對 Bob 提供給客戶最複雜的報告來說,Bob 需要同時依類別和期間來篩選記錄。 也就是說,Bob 想知道特定期間內某類別記錄的支出總額。
實作CategoryExpenses函式,回傳特定期間內某類別的支出總額。
這個函式還必須區分兩種情況:給定的類別不存在於支出記錄中,以及提供的期間內沒有該類別的支出。
當某個類別不是任何一筆記錄的類別時(不論期間為何),這個函式應該回傳錯誤。
p1 := DaysPeriod{From: 1, To: 30}
p2 := DaysPeriod{From: 31, To: 60}
records := []Record{
{Day: 1, Amount: 15, Category: "groceries"},
{Day: 11, Amount: 300, Category: "utility-bills"},
{Day: 12, Amount: 28, Category: "groceries"},
{Day: 26, Amount: 300, Category: "university"},
{Day: 28, Amount: 1300, Category: "rent"},
}
CategoryExpenses(records, p1, "entertainment")
// => 0, error(unknown category entertainment)
CategoryExpenses(records, p1, "rent")
// => 1300, nil
CategoryExpenses(records, p2, "rent")
// => 0, nil