支出

支出

學習練習

簡介

在 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

1. 實作通用的記錄篩選器

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"}
// ]

2. 篩選特定期間內的記錄

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"},
// ]

3. 依類別篩選記錄

除了依期間篩選記錄,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"},
// ]

4. 計算某期間內的支出總額

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

5. 計算某期間內某類別記錄的支出總額

對 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
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Go Exercism

準備好開始 支出 了嗎?

註冊 Exercism,透過 34 個概念165 個練習 和真人引導來學習並精通 Go,全部免費。