|
|
3 days ago | |
|---|---|---|
| .. | ||
| README.md | 3 days ago | |
| main.go | 3 days ago | |
学会把函数赋给变量、把函数作为参数,以及用闭包保存状态。理解上一课 defer 参数求值与闭包读取变量的区别,为下一课单元测试专题的 t.Run 做准备。
先手敲 main.go,每完成一个示例就运行。本课仅使用标准库 fmt,共享根目录的 go.mod,无需第三方依赖,不引入 goroutine 或复杂回调框架。
operation := add
result := operation(2, 3)
add:取得函数值。add(2, 3):立即调用函数,得到计算结果。add 的参数是两个 int,返回一个 int,所以其函数类型写作 func(int, int) int。参数名不影响这里的函数类型,参数和返回值类型、数量及可变参数形式等需要匹配。
可以先声明变量:
var operation func(int, int) int
operation = add
赋值前 operation 的零值是 nil,直接调用会 panic。函数值可以与 nil 比较,但不能用 f == g 比较两个非 nil 函数值是否相同。
double := func(number int) int {
return number * 2
}
匿名函数没有单独的函数名,结构仍然包含 func、参数、返回类型和函数体。创建函数值并赋给 double 后,要写 double(4) 才会执行。
也可以立即调用:
func(name string) {
fmt.Println(name)
}("小朱")
最后的 ("小朱") 是调用参数。没有最后这对调用括号时,只有函数值,不是执行它。
func calculate(a, b int, op func(int, int) int) int {
return op(a, b)
}
把它拆开看:
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,但本课暂不增加额外错误处理代码。
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,这种函数与所引用环境的组合称为闭包。
alias := counter,两个函数变量会共享同一个闭包状态,不会自动复制计数器。匿名函数不一定使用外层局部变量,不要把“匿名函数”与“保存外层状态”强行当作同一件事。
value := 10
defer fmt.Println(value)
defer func() {
fmt.Println(value)
}()
value = 20
第一个 defer 登记时,普通参数 value 已求值为 10。
第二个 defer 登记了匿名函数调用,匿名函数没有显式参数;它通过闭包引用外层 value,等执行函数体时读取到 20。
又因为后登记的先执行,所以返回时先输出闭包读取的 20,再输出普通参数保存的 10。defer 规则没有改变:调用的函数值和显式参数在登记时求值,函数体延后执行。
t.Run("加法", func(t *testing.T) {
got := add(2, 3)
if got != 5 {
t.Fatalf("实际结果:%d,期望:5", got)
}
})
现在可以按三层读:
t.Run(...):调用测试管理器的方法。"加法":子测试名称。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
alias := counter,交替调用 alias 和 counter,观察共享状态。defer func(v int) { fmt.Println(v) }(value),观察输出是否变成登记时的 10。若另行手敲,放到本课的 practice/ 子目录,用 go run ./lessons/13_function_values/practice 运行,不要在同一个包中重复声明 main 或同名函数。
下一课是单元测试专题,正式学习 testing.T、表驱动测试和 t.Run。