Kodewick
开始学 →
第 16 课,共 24 课目录

函数值和闭包

第 16 课,共 24 课 · 第 4 部分 · 出错和函数 · 约 15 分钟

Go 里的函数是一个值,和数、字符串一样。可以把它放进变量,再通过这个变量调用:

prog.go
1package main2 3import "fmt"4 5func double(n int) int {6	return n * 27}8 9func square(n int) int {10	return n * n11}12 13func main() {14	f := double15	fmt.Println(f(5))16	f = square17	fmt.Println(f(5))18	fmt.Printf("%T\n", f)19}
输出
10
25
func(int) int

注意 f := double 没有括号:这是函数本身,不是调用它。它的类型是 func(int) int,意思是「收一个 int、返回一个 int 的函数」。这个样子的函数都能放进 f。

函数作参数

既然函数是值,就能把它传给另一个函数。apply 不知道要对每个数做什么,由调用的地方决定:

prog.go
1package main2 3import "fmt"4 5func apply(nums []int, f func(int) int) []int {6	result := make([]int, len(nums))7	for i, n := range nums {8		result[i] = f(n)9	}10	return result11}12 13func double(n int) int {14	return n * 215}16 17func square(n int) int {18	return n * n19}20 21func main() {22	prices := []int{3, 5, 10}23	fmt.Println(apply(prices, double))24	fmt.Println(apply(prices, square))25}
输出
[6 10 20]
[9 25 100]

匿名函数

只传一次的函数,还要专门起名字写在外面,太麻烦。函数字面量是当场写出来、没有名字的函数:func(n int) int { return n + 1 }。它可以直接写在调用里:

prog.go
1package main2 3import "fmt"4 5func apply(nums []int, f func(int) int) []int {6	result := make([]int, len(nums))7	for i, n := range nums {8		result[i] = f(n)9	}10	return result11}12 13func main() {14	prices := []int{20, 50, 100}15	withTax := apply(prices, func(p int) int {16		return p * 110 / 10017	})18	fmt.Println(withTax)19	half := func(n int) int { return n / 2 }20	fmt.Println(half(9), apply(prices, half))21}
输出
[22 55 110]
4 [10 25 50]

再看 sort.Slice

第 10 课那一小段函数现在就好懂了。sort.Slice 收切片和一个函数 less(i, j int) bool,这个函数回答「第 i 个要不要排在第 j 个前面」。换一个函数,就换了一种顺序:

prog.go
1package main2 3import (4	"fmt"5	"sort"6)7 8type Product struct {9	Name  string10	Price int11}12 13func main() {14	items := []Product{{Name: "台灯", Price: 30}, {Name: "椅子", Price: 45}, {Name: "书桌", Price: 120}}15	sort.Slice(items, func(i, j int) bool {16		return items[i].Price > items[j].Price17	})18	fmt.Println(items)19	sort.Slice(items, func(i, j int) bool {20		return items[i].Name < items[j].Name21	})22	fmt.Println(items)23}
输出
[{书桌 120} {椅子 45} {台灯 30}]
[{书桌 120} {台灯 30} {椅子 45}]

第一次按价格排,贵的在前;第二次按名字排(汉字按编码比,不是按拼音或笔画)。这个函数用到了 items,一个 main 里的变量,这就引出了下一节。

闭包

函数字面量能用它周围的变量,而且不只是读:它用的是那个变量本身,不是一份复制。这样的函数叫闭包。点单步,看每次运行 add,main 里的 total 都跟着变:

prog.go
1package main2 3import "fmt"4 5func main() {6	total := 07	add := func(price int) {8		total += price9	}10	add(12)11	add(30)12	add(8)13	fmt.Println("合计", total)14}
输出
合计 50

会记数的计数器

做出变量的那个函数已经返回了,闭包还留着这些变量。makeCounter 做出 count,返回一个用它的闭包,然后结束;闭包手里还有 count。点单步,看两次调用之间 count 一直往上涨:

prog.go
1package main2 3import "fmt"4 5func makeCounter() func() int {6	count := 07	return func() int {8		count++9		return count10	}11}12 13func main() {14	next := makeCounter()15	fmt.Println(next())16	fmt.Println(next())17	fmt.Println(next())18}
输出
1
2
3
先猜一猜

这段程序显示什么?

1package main2 3import "fmt"4 5func makeCounter() func() int {6	count := 07	return func() int {8		count++9		return count10	}11}12 13func main() {14	a := makeCounter()15	b := makeCounter()16	a()17	a()18	fmt.Println(a(), b())19}

每一轮是新的变量

在循环里做闭包会怎样?从 Go 1.22 起,for 循环每一轮都有一个新的循环变量,所以每个闭包留着自己的 i:

prog.go
1package main2 3import "fmt"4 5func main() {6	var later []func()7	for i := range 3 {8		later = append(later, func() {9			fmt.Println("任务", i)10		})11	}12	for _, f := range later {13		f()14	}15}
输出
任务 0
任务 1
任务 2

这些函数在循环结束以后才运行,每个还是显示自己的号。Go 1.22 以前整个循环共用一个 i,三个都会显示 任务 3;在旧文章里可能会看到这种说法。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

用函数来筛

写好 filter:返回一个新切片,只留下 keep(n) 是 true 的数。程序用两个不同的函数筛了两次。

要显示
[4 8 42]
[15 23 42]
exercise1.go
1package main2 3import "fmt"4 5func filter(nums []int, keep func(int) bool) []int {6	var result []int7	return result8}9 10func main() {11	nums := []int{4, 15, 8, 23, 42, 7}12	fmt.Println(filter(nums, func(n int) bool { return n%2 == 0 }))13	fmt.Println(filter(nums, func(n int) bool { return n > 10 }))14}
输出
点「运行」或按 Ctrl+Enter
练习 2

两个人各记各的账

写好 makeTab:它返回一个函数,这个函数把价格加到累计的总数上,再返回新的总数。每本账记自己的总数。

要显示
小林 24
小陈 15
exercise2.go
1package main2 3import "fmt"4 5func makeTab() func(int) int {6	return func(price int) int {7		return price8	}9}10 11func main() {12	alice := makeTab()13	bob := makeTab()14	alice(12)15	bob(5)16	alice(8)17	fmt.Println("小林", alice(4))18	fmt.Println("小陈", bob(10))19}
输出
点「运行」或按 Ctrl+Enter
练习 3

修好它:每次都从头数的计数器

每次调用 next() 都显示 1。挪动一行,让计数器接着往上数:next 显示 1、2、3,other 显示 1。

要显示
1
2
3
1
exercise3.go
1package main2 3import "fmt"4 5func makeCounter() func() int {6	return func() int {7		count := 08		count++9		return count10	}11}12 13func main() {14	next := makeCounter()15	fmt.Println(next())16	fmt.Println(next())17	fmt.Println(next())18	other := makeCounter()19	fmt.Println(other())20}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 函数是值:f := double(不加括号);类型写成 func(int) int 这样。
  • 函数可以收另一个函数作参数,比如 apply(nums, f)、sort.Slice(s, less)。
  • 函数字面量 func(…) … { … } 没有名字,可以直接写在调用里。
  • 闭包用的是周围的变量本身,不是复制。
  • 做出变量的函数返回以后,闭包还留着这些变量:每次 makeCounter() 都有自己的 count。
  • 从 Go 1.22 起,for 循环每一轮都是一个新的循环变量。
这一课学完了吗?点一下标记。进度存在这个浏览器里,随时可以撤销。