在 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
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 想知道某个类别在给定时间段内的支出总额。
实现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