zhuts c2168558e2 feat: 新增第十三课函数值匿名函数与闭包例程 3 days ago
..
README.md c2168558e2 feat: 新增第十三课函数值匿名函数与闭包例程 3 days ago
main.go c2168558e2 feat: 新增第十三课函数值匿名函数与闭包例程 3 days ago

README.md

第 13 课:函数值、匿名函数与闭包

学习目标

学会把函数赋给变量、把函数作为参数,以及用闭包保存状态。理解上一课 defer 参数求值与闭包读取变量的区别,为下一课单元测试专题的 t.Run 做准备。

先手敲 main.go,每完成一个示例就运行。本课仅使用标准库 fmt,共享根目录的 go.mod,无需第三方依赖,不引入 goroutine 或复杂回调框架。

1. 函数也可以是值

operation := add
result := operation(2, 3)
  • add:取得函数值。
  • add(2, 3):立即调用函数,得到计算结果。
  • operation 是保存函数值的普通变量,不是定义了一个新具名函数。

add 的参数是两个 int,返回一个 int,所以其函数类型写作 func(int, int) int。参数名不影响这里的函数类型,参数和返回值类型、数量及可变参数形式等需要匹配。

可以先声明变量:

var operation func(int, int) int
operation = add

赋值前 operation 的零值是 nil,直接调用会 panic。函数值可以与 nil 比较,但不能用 f == g 比较两个非 nil 函数值是否相同。

2. 匿名函数

double := func(number int) int {
    return number * 2
}

匿名函数没有单独的函数名,结构仍然包含 func、参数、返回类型和函数体。创建函数值并赋给 double 后,要写 double(4) 才会执行。

也可以立即调用:

func(name string) {
    fmt.Println(name)
}("小朱")

最后的 ("小朱") 是调用参数。没有最后这对调用括号时,只有函数值,不是执行它。

3. 函数作为参数

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):调用传进来的函数,再把结果返回。

调用时可以传具名函数:

calculate(8, 3, add)

也可以直接传匿名函数:

calculate(8, 3, func(a, b int) int {
    return a - b
})

最后的 }) 分别关闭匿名函数体和 calculate 调用的参数列表。匿名函数在这里是交给 calculate 的参数,具体执行时机由 calculate 决定,本例会立即调用一次。

本课 calculate 约定 op 非 nil;传 nil 会 panic。实际接口可以根据需求检查 nil 并返回 error,但本课暂不增加额外错误处理代码。

4. 返回函数与闭包

func newCounter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

newCounter 本身不接收参数,返回值类型是 func() int,即一个“不接收参数、返回整数”的函数。

counter := newCounter() // 创建计数器,得到函数值。
fmt.Println(counter()) // 调用得到 1。
fmt.Println(counter()) // 再调用得到 2。

内部匿名函数使用了外层 count,这种函数与所引用环境的组合称为闭包。

  • 捕获的是变量,不是永远固定为创建时的数值。
  • 即使 newCounter 已返回,只要闭包仍需要 count,它就仍然有效,不会成为 C 风格的悬空局部变量。
  • 每次调用 newCounter 都创建独立的 count。
  • 如果写 alias := counter,两个函数变量会共享同一个闭包状态,不会自动复制计数器。
  • 共享闭包变量不等于线程安全,后续并发课程再学习同步。

匿名函数不一定使用外层局部变量,不要把“匿名函数”与“保存外层状态”强行当作同一件事。

5. 再看 defer 的两种情况

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 的写法

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,而是把函数本身传进去。这里不在匿名函数末尾写 (),否则就变成自行调用了。

这段是测试语法预习,不包含在本课可运行程序中;下一课再实际编写和执行子测试。

编译并运行

在仓库根目录执行:

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

修改源码后需要重新编译。快速编译并运行:

go run ./lessons/13_function_values

或从根目录进入本课:

Set-Location lessons/13_function_values
go run .

预期输出

=== 函数赋给变量 ===
通过变量调用: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 · 学习总览