函数值和闭包
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 不知道要对每个数做什么,由调用的地方决定:
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 }。它可以直接写在调用里:
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 个前面」。换一个函数,就换了一种顺序:
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 都跟着变:
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 一直往上涨:
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}
每调用一次 makeCounter 就做出一个新的 count。a 有自己的,数到了 3;b 有另一个,第一次调用后是 1。
每一轮是新的变量
在循环里做闭包会怎样?从 Go 1.22 起,for 循环每一轮都有一个新的循环变量,所以每个闭包留着自己的 i:
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;在旧文章里可能会看到这种说法。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
用函数来筛
✓写好 filter:返回一个新切片,只留下 keep(n) 是 true 的数。程序用两个不同的函数筛了两次。
[4 8 42] [15 23 42]
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}
两个人各记各的账
✓写好 makeTab:它返回一个函数,这个函数把价格加到累计的总数上,再返回新的总数。每本账记自己的总数。
小林 24 小陈 15
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}
修好它:每次都从头数的计数器
✓每次调用 next() 都显示 1。挪动一行,让计数器接着往上数:next 显示 1、2、3,other 显示 1。
1 2 3 1
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}
这一课学会了
- 函数是值:
f := double(不加括号);类型写成func(int) int这样。 - 函数可以收另一个函数作参数,比如
apply(nums, f)、sort.Slice(s, less)。 - 函数字面量
func(…) … { … }没有名字,可以直接写在调用里。 - 闭包用的是周围的变量本身,不是复制。
- 做出变量的函数返回以后,闭包还留着这些变量:每次
makeCounter()都有自己的count。 - 从 Go 1.22 起,
for循环每一轮都是一个新的循环变量。