Ver código fonte

feat: 新增第十三课函数值匿名函数与闭包例程

zhuts 3 dias atrás
pai
commit
c2168558e2
3 arquivos alterados com 287 adições e 3 exclusões
  1. 7 3
      README.md
  2. 214 0
      lessons/13_function_values/README.md
  3. 66 0
      lessons/13_function_values/main.go

+ 7 - 3
README.md

@@ -46,9 +46,12 @@
     ├── 11_errors/          # 第 11 课:错误处理
     │   ├── README.md       # error、nil、检查与错误传递
     │   └── main.go         # 除法、成绩校验和字符串转换
-    └── 12_switch_defer/    # 第 12 课:switch 与 defer
-        ├── README.md       # 多分支、延迟调用与执行顺序
-        └── main.go         # 指令选择、成绩判断和 defer 示例
+    ├── 12_switch_defer/    # 第 12 课:switch 与 defer
+    │   ├── README.md       # 多分支、延迟调用与执行顺序
+    │   └── main.go         # 指令选择、成绩判断和 defer 示例
+    └── 13_function_values/ # 第 13 课:函数进阶
+        ├── README.md       # 函数值、匿名函数、闭包与 t.Run 预习
+        └── main.go         # 回调计算、独立计数器和 defer 对比
 ```
 
 根目录不放例程代码。各课共享根目录的 `go.mod`,不需要逐课初始化模块,也不需要 `go.work`。当前模块路径 `example.com/go-study` 是学习用标识,不必与 Git 地址相同。
@@ -69,6 +72,7 @@
 | 10 | [方法与接收者](./lessons/10_methods/README.md) | 方法声明、值接收者、指针接收者、nil 处理 | 已建立 |
 | 11 | [错误处理](./lessons/11_errors/README.md) | error、nil、errors.New、fmt.Errorf、错误传递 | 已建立 |
 | 12 | [switch 与 defer](./lessons/12_switch_defer/README.md) | 多分支、延迟调用、后进先出、参数求值 | 已建立 |
+| 13 | [函数进阶](./lessons/13_function_values/README.md) | 函数值、匿名函数、函数参数、闭包 | 已建立 |
 
 后续按编号逐步新增独立目录。不一次性铺开后续课程,也不把新例程写进旧例程的入口文件。
 

+ 214 - 0
lessons/13_function_values/README.md

@@ -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)

+ 66 - 0
lessons/13_function_values/main.go

@@ -0,0 +1,66 @@
+package main
+
+import "fmt"
+
+func add(a, b int) int {
+	return a + b
+}
+
+// 另一个函数也可以作为参数,op 的类型是 func(int, int) int。
+// 本课约定调用方必须传入非 nil 的 op。
+func calculate(a, b int, op func(int, int) int) int {
+	return op(a, b)
+}
+
+// 返回值本身是函数。返回的匿名函数使用外层的 count,形成闭包。
+func newCounter() func() int {
+	count := 0
+	return func() int {
+		count++
+		return count
+	}
+}
+
+// 对比 defer 的普通参数与闭包读取变量的时机。
+func showDefer() {
+	value := 10
+	defer fmt.Printf("普通参数:%d\n", value)
+	defer func() {
+		fmt.Printf("闭包读取变量:%d\n", value)
+	}() // 这里的 () 表示调用;defer 把这次调用延后执行。
+	value = 20
+	fmt.Printf("函数主体:%d\n", value)
+}
+
+func main() {
+	fmt.Println("=== 函数赋给变量 ===")
+	operation := add // 不加括号是取得函数值,不是执行 add。
+	fmt.Printf("通过变量调用:%d\n", operation(2, 3))
+
+	fmt.Println("\n=== 匿名函数 ===")
+	double := func(number int) int {
+		return number * 2
+	}
+	fmt.Printf("匿名函数赋给变量:%d\n", double(4))
+	func(name string) {
+		fmt.Printf("立即调用:你好,%s\n", name)
+	}("小朱")
+
+	fmt.Println("\n=== 函数作为参数 ===")
+	fmt.Printf("传入加法函数:%d\n", calculate(8, 3, add))
+	result := calculate(8, 3, func(a, b int) int {
+		return a - b
+	})
+	fmt.Printf("传入匿名减法函数:%d\n", result)
+
+	fmt.Println("\n=== 闭包保存状态 ===")
+	counter := newCounter()
+	fmt.Printf("第一个计数器:%d\n", counter())
+	fmt.Printf("第一个计数器:%d\n", counter())
+	other := newCounter()
+	fmt.Printf("独立计数器:%d\n", other())
+	fmt.Printf("第一个计数器继续:%d\n", counter())
+
+	fmt.Println("\n=== 闭包与 defer ===")
+	showDefer()
+}