|
|
@@ -0,0 +1,214 @@
|
|
|
+# 第 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)
|