# 第 13 课:函数值、匿名函数与闭包 ## 学习目标 学会把函数赋给变量、把函数作为参数,以及用闭包保存状态。理解上一课 defer 参数求值与闭包读取变量的区别,为下一课单元测试专题的 t.Run 做准备。 先手敲 [main.go](./main.go),每完成一个示例就运行。本课仅使用标准库 fmt,共享根目录的 `go.mod`,无需第三方依赖,不引入 goroutine 或复杂回调框架。 ## 1. 函数也可以是值 ```go operation := add result := operation(2, 3) ``` - `add`:取得函数值。 - `add(2, 3)`:立即调用函数,得到计算结果。 - operation 是保存函数值的普通变量,不是定义了一个新具名函数。 add 的参数是两个 int,返回一个 int,所以其函数类型写作 `func(int, int) int`。参数名不影响这里的函数类型,参数和返回值类型、数量及可变参数形式等需要匹配。 可以先声明变量: ```go var operation func(int, int) int operation = add ``` 赋值前 operation 的零值是 nil,直接调用会 panic。函数值可以与 nil 比较,但不能用 `f == g` 比较两个非 nil 函数值是否相同。 ## 2. 匿名函数 ```go double := func(number int) int { return number * 2 } ``` 匿名函数没有单独的函数名,结构仍然包含 func、参数、返回类型和函数体。创建函数值并赋给 double 后,要写 `double(4)` 才会执行。 也可以立即调用: ```go func(name string) { fmt.Println(name) }("小朱") ``` 最后的 `("小朱")` 是调用参数。没有最后这对调用括号时,只有函数值,不是执行它。 ## 3. 函数作为参数 ```go func calculate(a, b int, op func(int, int) int) int { return op(a, b) } ``` 把它拆开看: - a、b:普通整数参数。 - op:参数名。 - `func(int, int) int`:op 的类型,说明它必须能接收两个整数并返回一个整数。 - `return op(a, b)`:调用传进来的函数,再把结果返回。 调用时可以传具名函数: ```go calculate(8, 3, add) ``` 也可以直接传匿名函数: ```go calculate(8, 3, func(a, b int) int { return a - b }) ``` 最后的 `})` 分别关闭匿名函数体和 calculate 调用的参数列表。匿名函数在这里是交给 calculate 的参数,具体执行时机由 calculate 决定,本例会立即调用一次。 本课 calculate 约定 op 非 nil;传 nil 会 panic。实际接口可以根据需求检查 nil 并返回 error,但本课暂不增加额外错误处理代码。 ## 4. 返回函数与闭包 ```go func newCounter() func() int { count := 0 return func() int { count++ return count } } ``` newCounter 本身不接收参数,返回值类型是 `func() int`,即一个“不接收参数、返回整数”的函数。 ```go counter := newCounter() // 创建计数器,得到函数值。 fmt.Println(counter()) // 调用得到 1。 fmt.Println(counter()) // 再调用得到 2。 ``` 内部匿名函数使用了外层 count,这种函数与所引用环境的组合称为闭包。 - 捕获的是变量,不是永远固定为创建时的数值。 - 即使 newCounter 已返回,只要闭包仍需要 count,它就仍然有效,不会成为 C 风格的悬空局部变量。 - 每次调用 newCounter 都创建独立的 count。 - 如果写 `alias := counter`,两个函数变量会共享同一个闭包状态,不会自动复制计数器。 - 共享闭包变量不等于线程安全,后续并发课程再学习同步。 匿名函数不一定使用外层局部变量,不要把“匿名函数”与“保存外层状态”强行当作同一件事。 ## 5. 再看 defer 的两种情况 ```go value := 10 defer fmt.Println(value) defer func() { fmt.Println(value) }() value = 20 ``` 第一个 defer 登记时,普通参数 value 已求值为 10。 第二个 defer 登记了匿名函数调用,匿名函数没有显式参数;它通过闭包引用外层 value,等执行函数体时读取到 20。 又因为后登记的先执行,所以返回时先输出闭包读取的 20,再输出普通参数保存的 10。defer 规则没有改变:调用的函数值和显式参数在登记时求值,函数体延后执行。 ## 6. 预习 t.Run 的写法 ```go t.Run("加法", func(t *testing.T) { got := add(2, 3) if got != 5 { t.Fatalf("实际结果:%d,期望:5", got) } }) ``` 现在可以按三层读: 1. `t.Run(...)`:调用测试管理器的方法。 2. 第一个参数 `"加法"`:子测试名称。 3. 第二个参数 `func(t *testing.T) { ... }`:匿名函数,交给测试框架执行。 不是把匿名函数的返回结果传给 Run,而是把函数本身传进去。这里不在匿名函数末尾写 `()`,否则就变成自行调用了。 这段是测试语法预习,不包含在本课可运行程序中;下一课再实际编写和执行子测试。 ## 编译并运行 在**仓库根目录**执行: ```powershell New-Item -ItemType Directory -Force bin/13_function_values | Out-Null go build -o ./bin/13_function_values/function_values.exe ./lessons/13_function_values .\bin\13_function_values\function_values.exe ``` 修改源码后需要重新编译。快速编译并运行: ```powershell go run ./lessons/13_function_values ``` 或从根目录进入本课: ```powershell Set-Location lessons/13_function_values go run . ``` ## 预期输出 ```text === 函数赋给变量 === 通过变量调用:5 === 匿名函数 === 匿名函数赋给变量:8 立即调用:你好,小朱 === 函数作为参数 === 传入加法函数:11 传入匿名减法函数:5 === 闭包保存状态 === 第一个计数器:1 第一个计数器:2 独立计数器:1 第一个计数器继续:3 === 闭包与 defer === 函数主体:20 闭包读取变量:20 普通参数:10 ``` ## 动手练习 - [ ] 把 double 改为计算三倍值。 - [ ] 给 calculate 传入匿名乘法函数,核对结果。 - [ ] 创建第三个计数器,确认它从 1 开始。 - [ ] 写 `alias := counter`,交替调用 alias 和 counter,观察共享状态。 - [ ] 给闭包 defer 增加显式参数,改成 `defer func(v int) { fmt.Println(v) }(value)`,观察输出是否变成登记时的 10。 - [ ] 回看第七课的 t.Run,把子测试名称与匿名函数两个参数分别找出来。 若另行手敲,放到本课的 `practice/` 子目录,用 `go run ./lessons/13_function_values/practice` 运行,不要在同一个包中重复声明 main 或同名函数。 下一课是单元测试专题,正式学习 testing.T、表驱动测试和 t.Run。 [上一课:switch 与 defer](../12_switch_defer/README.md) · [学习总览](../../README.md)