轨道
/
Go
Go
/
练习
/
支出
支出

支出

学习练习

简介

在 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 纪元来代替日期。 Bob 纪元是自 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 想知道某个类别在给定时间段内的支出总额。

实现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 个概念166 个练习 和真人导师指导,学习并掌握 Go,全部免费。