本系列旨在梳理 Go 的 release notes 与发展史,来更加深入地理解 Go 语言设计的思路。
https://go.dev/doc/go1.21
Go 1.21 值得关注的改动:
- 版本号规则变更:Go 1.21 开始,首个版本号将标记为 1.N.0 而不是之前的 1.N,例如 Go 1.21 的首个版本是 go1.21.0。
- 新增内置函数:添加了 min、max 用于比较大小,以及 clear 用于清空 map 或归零 slice 的元素。
- 类型推断增强:改进了类型推断(type inference)的能力和精度,使其更强大,推断失败的情况也更符合预期。
- for 循环变量作用域实验性调整:预览了一个未来的语言变更,旨在让 for 循环变量的作用域变为每次迭代(per-iteration),以避免常见的因变量共享(variable sharing)导致的 bug。
- panic/recover 行为变更:现在 defer 中直接调用 recover 时保证返回值非 nil;panic(nil) 会引发 *runtime.PanicNilError 类型的运行时 panic。
- 运行时与垃圾回收(Garbage Collection, GC)优化:改进了 Linux 平台上的透明大页(transparent huge pages)管理,优化了 GC 调优,可能带来显著的尾延迟(tail latency)降低和内存使用减少,但可能伴随少量吞吐量(throughput)下降。
- 新增 log/slog 包:提供支持级别的结构化日志(structured logging)功能。
- 新增 testing/slogtest 包:用于帮助验证 slog.Handler 的实现。
- 新增泛型工具包:引入了 slices、maps 和 cmp 包,提供了对切片、映射和有序类型的泛型操作函数。
下面是一些值得展开的讨论:
类型推断的增强与细化
Go 1.21 对类型推断进行了多项改进,使其更加强大和精确,同时也澄清了语言规范中关于类型推断的描述。这些变化使得类型推断失败的情况更少,也更容易理解。
主要的改进包括:
- 支持将泛型函数作为参数传递给其他泛型函数 :现在,一个(可能部分实例化的)泛型函数可以接受本身也是(可能部分实例化的)泛型函数的参数。编译器会尝试推断调用者(callee)缺失的类型参数(和以前一样),并且(新增地)也会为作为参数传入的、未完全实例化的泛型函数推断其缺失的类型参数。
一个典型的场景是调用操作容器的泛型函数(例如 slices.IndexFunc),其函数参数本身也可能是泛型的。调用函数和其参数的类型参数可以从容器类型中推断出来。- package main
- import (
- "fmt"
- "slices"
- "strings"
- )
- var prefix string = "ap"
- // 一个泛型函数,检查字符串是否以特定前缀开头
- func HasPrefix[E ~string](s E) bool {
- return strings.HasPrefix(string(s), prefix)
- }
- func main() {
- strs := []string{"apple", "banana", "apricot"}
- // 在 Go 1.21 之前,直接传递 HasPrefix 可能需要显式实例化类型参数,
- // 或者编译器可能无法推断。
- // index := slices.IndexFunc(strs, func(s string) bool { // 需要定义一个闭包
- // return HasPrefix(s)
- // })
- // 在 Go 1.21 中,可以直接传递泛型函数 HasPrefix(部分实例化,省略了类型参数 E),
- // 编译器能够根据 strs 的类型 ( []string ) 推断出 E 应该是 string,
- // 同时也能推断出 slices.IndexFunc 的类型参数 S 应该是 string。
- index := slices.IndexFunc(strs, HasPrefix)
- // 修正:虽然文档描述了更强的推断,但实际例子中直接传 HasPrefix 仍然可能不工作
- // 最自然的用法还是通过闭包,闭包内部调用泛型函数会更容易推断
- // 或者更典型的例子是推断返回类型或赋值
- fmt.Println("Index of first string starting with 'ap':", index) // Output: 0
- // 另一个例子:泛型函数赋值
- // var myHasPrefix func(string) bool = HasPrefix // Go 1.21 可以推断
- // fmt.Println(myHasPrefix("app"))
- }
复制代码 修正说明 :虽然 release notes 描述了“函数可以接受泛型函数作为参数”,但最直接的 slices.IndexFunc(strs, HasPrefix) 这种形式可能仍然受限。更常见的改进体现在闭包内调用泛型函数,或将泛型函数赋值给变量/作为返回值时,类型参数能被上下文推断出来。
- 通过赋值给变量或作为返回值推断类型 :如果泛型函数的类型参数可以从赋值的目标类型或函数返回类型中推断出来,那么现在可以在不显式实例化的情况下使用它。
- package main
- import "fmt"
- func MakePair[F, S any](f F, s S) func() (F, S) {
- return func() (F, S) {
- return f, s
- }
- }
- func main() {
- // Go 1.21 可以从 p1 的类型推断出 MakePair 的 F 和 S
- var p1 func() (int, string)
- p1 = MakePair(10, "hello") // 推断 F=int, S=string
- f, s := p1()
- fmt.Println(f, s) // Output: 10 hello
- // 也可以直接在 return 语句中使用
- getP2 := func() func() (bool, float64) {
- return MakePair(true, 3.14) // 推断 F=bool, S=float64
- }
- p2 := getP2()
- b, fl := p2()
- fmt.Println(b, fl) // Output: true 3.14
- }
复制代码
- 通过接口方法匹配推断类型 :当一个值被赋给接口类型时,类型推断现在会考虑方法。如果类型参数用在方法签名中,其类型参数可以从匹配的接口方法对应的参数类型中推断出来。
- 通过约束的方法匹配推断类型 :类似地,由于类型实参必须实现其对应约束的所有方法,类型实参的方法和约束的方法会被匹配,这可能导致推断出额外的类型参数。
- 处理混合类型的无类型常量参数 :如果多个不同种类的无类型常量(例如,一个无类型 int 和一个无类型 float)被传递给具有相同(未指定)类型参数类型的参数,现在类型推断会使用与处理无类型常量操作数的操作符相同的规则来确定类型,而不是报错。这使得从无类型常量参数推断出的类型与常量表达式的类型保持一致。
- package main
- import "fmt"
- // F 的类型参数 T 会根据传入的 x 和 y 推断
- func F[T any](x, y T) T {
- // 注意:这里不能直接做 x + y,因为 T 是 any,不支持 +
- // 这个例子主要演示类型推断,而不是运算
- fmt.Printf("In F: x type = %T, y type = %T, T inferred as = %T\n", x, y, *new(T))
- return x
- }
- func main() {
- // 在 Go 1.21 之前,传递 1 (untyped int) 和 2.0 (untyped float)
- // 给类型参数 T 会导致推断失败。
- // Go 1.21 中,会根据常量运算规则推断 T 为默认类型。
- // 对于 1 和 2.0,整数和浮点数混合,结果类型倾向于浮点数,默认是 float64。
- _ = F(1, 2.0) // 输出会显示 T 被推断为 float64
- _ = F(1, 2) // T 会被推断为 int
- }
复制代码- In F: x type = float64, y type = float64, T inferred as = float64
- In F: x type = int, y type = int, T inferred as = int
复制代码
- 赋值时精确匹配组件类型 :在匹配赋值中的对应类型时,类型推断现在更加精确:组件类型(如 slice 的元素类型,或函数签名的参数类型)必须(在给定合适的类型参数后)完全相同才能匹配,否则推断失败。这一改变会产生更准确的错误信息:过去类型推断可能错误地成功,导致无效的赋值,而现在编译器会在两种类型不可能匹配时报告推断错误。
实验性的 for 循环变量语义变更
Go 1.21 包含了一项针对未来 Go 版本考虑的语言变更预览:将 for 循环变量的作用域从“每次循环”(per-loop)改为“每次迭代”(per-iteration),以避免意外的变量共享(accidental sharing)导致的 bug。
旧行为(Go 1.21 默认及之前版本)
在传统的 for 循环中,循环变量(如 i 和 v 在 for i, v := range slice 中)在整个循环过程中是同一个变量,每次迭代只是更新它的值。如果在循环内部启动的 goroutine 中直接引用这个变量,很可能所有 goroutine 最终都引用到该变量的最后一个值。
[code]package mainimport ( "fmt" "sync" "time")func main() { var wg sync.WaitGroup nums := []int{1, 2, 3} fmt.Println("Default behavior (Go |