
本系列旨在梳理 Go 的 release notes 与发展史,来更加深入地理解 Go 语言设计的思路。
Go 1.21 值得关注的改动:
1.N.0 而不是之前的 1.N,例如 Go 1.21 的首个版本是 go1.21.0。min、max 用于比较大小,以及 clear 用于清空 map 或归零 slice 的元素。for 循环变量作用域实验性调整:预览了一个未来的语言变更,旨在让 for 循环变量的作用域变为每次迭代(per-iteration),以避免常见的因变量共享(variable sharing)导致的 bug。panic/recover 行为变更:现在 defer 中直接调用 recover 时保证返回值非 nil;panic(nil) 会引发 *runtime.PanicNilError 类型的运行时 panic。log/slog 包:提供支持级别的结构化日志(structured logging)功能。testing/slogtest 包:用于帮助验证 slog.Handler 的实现。slices、maps 和 cmp 包,提供了对切片、映射和有序类型的泛型操作函数。下面是一些值得展开的讨论:
Go 1.21 对类型推断进行了多项改进,使其更加强大和精确,同时也澄清了语言规范中关于类型推断的描述。这些变化使得类型推断失败的情况更少,也更容易理解。
主要的改进包括:
一个典型的场景是调用操作容器的泛型函数(例如 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 = intfor 循环变量语义变更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 最终都引用到该变量的最后一个值。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
nums := []int{1, 2, 3}
fmt.Println("Default behavior (Go <= 1.21 or without experiment):")
for _, v := range nums {
wg.Add(1)
go func() { // 这个 goroutine 捕获的是循环变量 v 的地址
defer wg.Done()
time.Sleep(10 * time.Millisecond) // 模拟延迟,确保循环结束
fmt.Printf("Goroutine sees v = %d\n", v)
}()
}
wg.Wait()
// 通常输出 (顺序不定):
// Goroutine sees v = 3
// Goroutine sees v = 3
// Goroutine sees v = 3
fmt.Println("\nCommon workaround:")
for _, v := range nums {
v := v // 创建一个当前迭代的局部副本
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(10 * time.Millisecond)
fmt.Printf("Goroutine sees v = %d\n", v) // 捕获的是副本 v
}()
}
wg.Wait()
// 输出 (顺序不定):
// Goroutine sees v = 1
// Goroutine sees v = 2
// Goroutine sees v = 3
}新的实验性行为
如果启用了 loopvar 实验,for 循环(包括 for-range 和三段式 for 循环)声明的变量在每次迭代时都会被认为是新声明的变量。这意味着闭包可以直接捕获每次迭代的变量,而无需手动创建副本。
要尝试这个新行为,需要设置环境变量 GOEXPERIMENT=loopvar 来编译和运行代码:
GOEXPERIMENT=loopvar go run your_code.go如果使用上述代码的第一部分(没有显式副本 v := v 的那段),并在启用 GOEXPERIMENT=loopvar 的情况下运行,其行为将如同使用了显式副本的版本,输出每个 goroutine 看到的值是 1, 2, 3(顺序不定)。
注意 :这在 Go 1.21 中仍然是 实验性 的。默认行为没有改变。这个变更是为了解决一个长期存在的 Go 新手陷阱。最终是否以及何时成为默认行为将在未来的版本中决定。
panic 与 recover 行为的明确化Go 1.21 对 panic 和 recover 的交互行为做了一个重要的明确化和保证。
核心变更
如果一个 goroutine 正在 panic,并且一个被 defer 的函数 直接 调用了 recover(),那么 recover() 的返回值 保证不是 nil。
如何保证
为了实现上述保证,Go 1.21 改变了 panic(nil) 的行为。在此版本之前,调用 panic(nil) 或 panic 一个值为 nil 的接口是合法的,但会导致后续直接调用的 recover() 返回 nil,使得无法区分是正常执行完毕还是由 panic(nil) 引起的恢复。
从 Go 1.21 开始,如果调用 panic 时传入一个 nil 接口值(或无类型的 nil),会引发一个运行时的 panic,其类型为 *runtime.PanicNilError。这是一个新的、非 nil 的错误类型。因此,即使原始的 panic 意图是传递 nil,实际传递给 recover 的值将是一个非 nil 的 *runtime.PanicNilError 实例。
示例对比
package main
import "fmt"
func main() {
fmt.Println("Testing panic(nil)")
defer func() {
fmt.Println("Deferred function starts.")
r := recover()
if r == nil {
fmt.Println("recover returned nil. Either no panic or panic(nil) in Go <= 1.20.")
} else {
fmt.Printf("recover returned non-nil: %T, value: %[1]v\n", r)
// 在 Go 1.21+,如果由 panic(nil) 触发,这里会捕获 *runtime.PanicNilError
}
fmt.Println("Deferred function ends.")
}()
fmt.Println("Calling panic(nil)...")
panic(nil) // 引发 panic
// 这行不会执行
fmt.Println("After panic call (should not be reached).")
}Testing panic(nil)
Calling panic(nil)...
Deferred function starts.
recover returned nil. Either no panic or panic(nil) in Go <= 1.20.
Deferred function ends.Testing panic(nil)
Calling panic(nil)...
Deferred function starts.
recover returned non-nil: *runtime.PanicNilError, value: panic called with nil argument
Deferred function ends.向后兼容性
为了支持为旧版本 Go 编写的、可能依赖 panic(nil) 后 recover 返回 nil 行为的程序,可以通过设置 GODEBUG=panicnil=1 来重新启用旧的行为(即 panic(nil) 不会转换成 *runtime.PanicNilError,recover 仍会返回 nil)。
此外,如果一个程序的主包(main package)所在的模块(module)在其 go.mod 文件中声明了 go 1.20 或更早的版本,编译器会自动启用 GODEBUG=panicnil=1 这个设置。
Go 1.21 在运行时和垃圾回收(GC)方面进行了一些重要的优化,主要集中在内存使用效率和延迟上。
透明大页(Transparent Huge Pages, THP)管理 (Linux)
enabled 或 defrag 设置有关)。如果操作系统配置不理想(例如,全局启用了 THP 但碎片整理成本很高),这可能反而导致更高的内存开销。madvise 而不是 always)。max_ptes_none (控制进程地址空间中不映射任何页表的区域大小),但这通常是更深层次的优化。内部 GC 调优
GOGC(GC 百分比)或 GOMEMLIMIT(内存限制)的值来恢复到接近之前版本的吞吐量/内存权衡点,同时很可能仍然能保留大部分的延迟改进。总的来说,Go 1.21 的运行时和 GC 优化旨在默认情况下提供更好的延迟特性和内存效率,同时为需要不同权衡的用户保留了调整空间。
log/slogGo 1.21 引入了一个全新的标准库包 log/slog,用于提供 结构化日志(structured logging) 功能。结构化日志旨在通过发出键值对(key-value pairs)而不是非结构化的文本消息,来方便机器进行快速、准确的处理和分析大量的日志数据。
为什么需要 slog?
在 slog 之前,Go 的标准库 log 包主要输出简单的文本行。虽然可以通过 fmt.Sprintf 或手动编码(如 JSON)来模拟结构化,但这缺乏标准、容易出错且效率不高。对于现代的日志聚合、分析系统(如 ELK Stack, Splunk, Datadog 等),结构化的日志格式是首选。
slog 的核心概念:
Debug, Info, Warn, Error),可以控制记录哪些级别的日志。slog.Attr),可以方便地添加上下文信息。slog.Handler 接口负责处理日志记录(Record),将其格式化(如文本、JSON)并写入输出(如 os.Stderr, 文件, 网络)。标准库提供了 slog.TextHandler 和 slog.JSONHandler。slog.Logger 是进行日志记录操作的入口点。可以创建具有预设属性或特定 Handler 的 Logger 实例。示例:对比 log 和 slog
假设我们要记录一个用户请求的处理信息,包括请求 ID、用户 ID 和处理时长。
之前使用 log 包(尝试模拟结构化)
package main
import (
"log"
"time"
//"encoding/json" // 或者使用 JSON
)
func main() {
requestID := "req-123"
userID := "user-456"
startTime := time.Now()
// 模拟处理
time.Sleep(50 * time.Millisecond)
duration := time.Since(startTime)
// 方式一:手动格式化字符串
log.Printf("INFO: request processing finished. request_id=%s user_id=%s duration=%s",
requestID, userID, duration)
// 方式二:尝试输出 JSON (更繁琐)
// entry := map[string]interface{}{
// "level": "INFO",
// "message": "request processing finished",
// "request_id": requestID,
// "user_id": userID,
// "duration_ms": duration.Milliseconds(),
// }
// jsonData, _ := json.Marshal(entry)
// log.Println(string(jsonData))
}2025/05/02 22:57:14 INFO: request processing finished. request_id=req-123 user_id=user-456 duration=54.537229ms使用 log/slog 包
package main
import (
"log/slog"
"os"
"time"
)
func main() {
// 创建一个使用默认 TextHandler 的 Logger,输出到 stderr
// logger := slog.Default() // 或者使用默认 logger
// 或者创建一个 JSON Handler
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) // 输出 JSON 到 stdout
requestID := "req-123"
userID := "user-456"
startTime := time.Now()
// 模拟处理
time.Sleep(50 * time.Millisecond)
duration := time.Since(startTime)
// 使用 Info 级别记录日志,并附带属性
logger.Info("request processing finished",
slog.String("request_id", requestID),
slog.String("user_id", userID),
slog.Duration("duration", duration), // slog 有 Duration 类型
)
// 也可以记录其他级别,例如警告
logger.Warn("potential issue detected",
slog.String("request_id", requestID),
slog.Int("error_code", 503),
)
}{"time":"2025-05-02T22:58:34.003809744+08:00","level":"INFO","msg":"request processing finished","request_id":"req-123","user_id":"user-456","duration":51303289}
{"time":"2025-05-02T22:58:34.007783138+08:00","level":"WARN","msg":"potential issue detected","request_id":"req-123","error_code":503}使用 slog,日志输出是结构化的,易于机器解析,并且 API 设计清晰,支持级别控制和灵活的处理器配置。
slog 的配套测试库:testing/slogtest伴随着 log/slog 包的引入,Go 1.21 还提供了一个新的测试包 testing/slogtest。这个包的主要目的是帮助开发者 验证自定义的 slog.Handler 实现 是否符合预期的行为规范。
为什么需要 slogtest?
当你创建自己的 slog.Handler 实现时(例如,你想把日志发送到特定的第三方服务,或者实现一种特殊的格式化逻辑),你需要确保你的 Handler 正确地处理了 slog 的各种特性,比如:
Info, Warn, Error, Debug)。String, Int, Bool, Time, Duration, Group, etc.)。WithAttrs 和 WithGroup 方法,维护属性上下文。没有 slogtest 如何测试?
io.Writer (比如 bytes.Buffer) 来捕获输出。slog.New(yourHandler) 创建一个 Logger 。Info, Warn, With, etc.)来模拟不同的日志场景。bytes.Buffer 中的内容。这个过程非常繁琐、容易出错,且难以覆盖所有 slog Handler 需要处理的边界情况。
使用 testing/slogtest 测试:
testing/slogtest 包提供了一个核心函数 TestHandler,它会自动运行一系列预定义的测试用例来覆盖 Handler 的各种行为。你只需要提供两个东西:
io.Writer 的内容)。示例:使用 slogtest
假设我们有一个(可能不完美的)自定义 Handler,它将日志记录简单地格式化为 Level: Message Key=Value ... 并写入提供的 io.Writer。
package mylogger_test
import (
"bytes"
"context"
"log/slog"
"strings"
"testing"
"testing/slogtest" // 引入测试包
)
// --- 我们想要测试的自定义 Handler (简单示例) ---
type MyHandler struct {
buf *bytes.Buffer
mu sync.Mutex // 假设需要并发安全
attrs []slog.Attr
group string
}
func NewMyHandler(buf *bytes.Buffer) *MyHandler {
return &MyHandler{buf: buf}
}
func (h *MyHandler) Enabled(ctx context.Context, level slog.Level) bool {
return true // 简单起见,总是启用
}
func (h *MyHandler) Handle(ctx context.Context, r slog.Record) error {
h.mu.Lock()
defer h.mu.Unlock()
var sb strings.Builder
sb.WriteString(r.Level.String())
sb.WriteString(": ")
sb.WriteString(r.Message)
allAttrs := h.attrs
r.Attrs(func(a slog.Attr) bool {
allAttrs = append(allAttrs, a)
return true
})
for _, attr := range allAttrs {
// 简单处理 group
key := attr.Key
if h.group != "" {
key = h.group + "." + key
}
sb.WriteString(" ")
sb.WriteString(key)
sb.WriteString("=")
sb.WriteString(attr.Value.String()) // 简化处理,都转为 String
}
sb.WriteString("\n")
_, err := h.buf.Write([]byte(sb.String()))
return err
}
func (h *MyHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
h.mu.Lock()
defer h.mu.Unlock()
newH := *h // 浅拷贝
newH.attrs = append(slices.Clip(h.attrs), attrs...) // copy on write
return &newH
}
func (h *MyHandler) WithGroup(name string) slog.Handler {
h.mu.Lock()
defer h.mu.Unlock()
newH := *h // 浅拷贝
if newH.group != "" {
newH.group += "." + name
} else {
newH.group = name
}
return &newH
}
// --- 测试代码 ---
func TestMyHandler(t *testing.T) {
var buf bytes.Buffer // 用于捕获 Handler 输出
// 提供创建 Handler 的函数
newHandler := func(*testing.T) slog.Handler {
// 注意:每次 TestHandler 调用这个函数时,都应该返回一个全新的 Handler 实例
buf.Reset() // 重置 buffer,确保每个子测试用例干净
return NewMyHandler(&buf)
}
// 提供获取结果的函数
results := func(*testing.T) map[string]any {
// 将 buffer 内容按行分割,模拟多个日志条目
// 注意:实际的 results 函数可能需要更复杂的解析逻辑,
// 取决于 TestHandler 的具体要求以及 Handler 的输出格式。
// slogtest 需要能理解这些结果来和预期进行比较。
// 对于复杂格式,可能需要解析为 map[string]any 或类似结构。
// 这里简化为直接返回字符串 map,key 通常是测试用例名。
// !!!重要:slogtest 的 results 函数需要返回 map[string]any
// 以便与内部的预期结果进行比较。简单的字符串分割可能不足以
// 通过所有测试。一个更健壮的方法是解析日志行回结构化数据。
// 为简单起见,我们这里仅演示流程,实际实现需要更细致的解析。
lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
resMap := make(map[string]any)
for i, line := range lines {
// 使用某种方式将 line 关联到 slogtest 内部的测试名
// 这通常比较困难,除非你的 handler 输出包含测试名信息
// 或者 slogtest 提供某种机制来关联输入和输出。
// 另一种常见做法是让 results 返回能够代表“状态”的结构。
resMap[fmt.Sprintf("line%d", i)] = line // 极简示例
}
return resMap
}
// 运行 slogtest 的测试套件
// 注意:这里需要传递一个 results 函数指针
// 由于 results 函数的正确实现依赖于对 slogtest 内部工作方式的理解
// (如何匹配日志输出和测试用例),以下调用可能需要调整 results 函数才能真正工作。
err := slogtest.TestHandler(newHandler(t), results) // 传递 results 函数指针
if err != nil {
t.Fatal(err)
}
// slogtest v0.3.0 开始推荐使用 t.Run 调用
// t.Run("slogtest", func(t *testing.T) {
// err := slogtest.TestHandler(newHandler(t), results)
// if err != nil {
// t.Fatal(err)
// }
// })
}slogtest.TestHandler 会自动执行一系列场景(如记录不同类型的值、使用 WithGroup, WithAttrs 等),并调用你提供的 results 函数来获取 Handler 的输出,然后与内部的预期结果进行比较。如果你的 Handler 实现有任何不符合规范的地方,TestHandler 会返回错误,指出哪里出了问题。
这极大地简化了自定义 Handler 的测试工作,提高了其可靠性。
slices, maps, cmp随着 Go 1.18 引入泛型,标准库也开始利用这一特性来提供更通用、类型安全的工具函数。Go 1.21 新增了三个重要的泛型包:slices、maps 和 cmp。
slices 包
这个包提供了许多对任意元素类型的 slice 进行操作的常用函数。
slices.Sort[S ~[]E, E cmp.Ordered](x S) : 对元素类型为有序类型(实现了 cmp.Ordered 约束,即支持 <、> 等操作符)的 slice 进行原地排序。package main
import (
"fmt"
"slices"
)
func main() {
ints := []int{3, 1, 4, 1, 5, 9}
slices.Sort(ints)
fmt.Println("Sorted ints:", ints) // Output: Sorted ints: [1 1 3 4 5 9]
strs := []string{"c", "a", "b"}
slices.Sort(strs)
fmt.Println("Sorted strs:", strs) // Output: Sorted strs: [a b c]
}slices.Contains[S ~[]E, E comparable](s S, v E) bool : 检查 slice s 是否包含元素 v。元素类型 E 必须是可比较的(comparable)。package main
import (
"fmt"
"slices"
)
func main() {
nums := []int{1, 2, 3, 4}
fmt.Println("Contains 3?", slices.Contains(nums, 3)) // Output: Contains 3? true
fmt.Println("Contains 5?", slices.Contains(nums, 5)) // Output: Contains 5? false
}slices.IndexFunc[S ~[]E, E any](s S, f func(E) bool) int : 返回 slice s 中第一个满足函数 f (predicate) 的元素的索引,如果找不到则返回 -1。元素类型 E 可以是任意类型 (any)。package main
import (
"fmt"
"slices"
"strings"
)
func main() {
strs := []string{"apple", "banana", "apricot"}
// 查找第一个以 "ap" 开头的字符串
index := slices.IndexFunc(strs, func(s string) bool {
return strings.HasPrefix(s, "ap")
})
fmt.Println("Index of first string starting with 'ap':", index) // Output: 0
}slices 包还包含许多其他有用的函数,如 BinarySearch, Compact, Delete, Equal, Insert, Max, Min, Reverse 等。
maps 包
这个包提供了对任意键(key)和值(value)类型的 map 进行操作的常用函数。
maps.Keys[M ~map[K]V, K comparable, V any](m M) []K : 返回 map m 的所有键组成的 slice。package main
import (
"fmt"
"maps"
"slices" // 需要用来排序以获得稳定输出
)
func main() {
m := map[string]int{"a": 1, "c": 3, "b": 2}
keys := maps.Keys(m)
slices.Sort(keys) // map 遍历顺序不定,排序后输出稳定
fmt.Println("Keys:", keys) // Output: Keys: [a b c]
}maps.Values[M ~map[K]V, K comparable, V any](m M) []V : 返回 map m 的所有值组成的 slice。package main
import (
"fmt"
"maps"
"slices" // 需要用来排序以获得稳定输出
)
func main() {
m := map[string]int{"a": 1, "c": 3, "b": 2}
values := maps.Values(m)
slices.Sort(values) // map 遍历顺序不定,排序后输出稳定
fmt.Println("Values:", values) // Output: Values: [1 2 3]
}maps.Clone[M ~map[K]V, K comparable, V any](m M) M : 创建并返回 map m 的一个浅拷贝(shallow copy)。package main
import (
"fmt"
"maps"
)
func main() {
m1 := map[string]int{"a": 1}
m2 := maps.Clone(m1)
m2["b"] = 2
fmt.Println("m1:", m1) // Output: m1: map[a:1]
fmt.Println("m2:", m2) // Output: m2: map[a:1 b:2]
}maps 包还有 Copy, DeleteFunc, Equal, EqualFunc 等函数。
cmp 包
这个包定义了一个重要的类型约束 cmp.Ordered,并提供了两个与有序类型相关的泛型函数。
cmp.Ordered : 这是一个接口类型约束,它约束类型参数必须是支持排序操作符(<, <=, >, >=)的类型。包括所有内置的整数、浮点数和字符串类型。type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}cmp.Compare[T Ordered](x, y T) int : 比较两个有序类型 x 和 y。如果 x < y 返回 -1,如果 x == y 返回 0,如果 x > y 返回 +1。package main
import (
"cmp"
"fmt"
)
func main() {
fmt.Println(cmp.Compare(1, 2)) // Output: -1
fmt.Println(cmp.Compare(2, 2)) // Output: 0
fmt.Println(cmp.Compare("b", "a")) // Output: 1
}cmp.Less[T Ordered](x, y T) bool : 检查是否有序类型 x 小于 y。等价于 x < y。package main
import (
"cmp"
"fmt"
"slices"
)
func main() {
fmt.Println(cmp.Less(1, 2)) // Output: true
fmt.Println(cmp.Less("a", "b")) // Output: true
// 可以用于 slices.SortFunc
nums := []int{3, 1, 4}
slices.SortFunc(nums, cmp.Compare[int]) // 使用 Compare 作为比较函数
// 或者 slices.SortFunc(nums, func(a, b int) int { return cmp.Compare(a,b) })
fmt.Println("Sorted with cmp.Compare:", nums) // Output: [1 3 4]
}cmp.Compare 和 cmp.Less 为处理有序类型提供了标准的泛型函数,尤其在与其他泛型函数(如 slices.SortFunc, slices.BinarySearchFunc)配合使用时非常方便。
这些新的泛型包大大增强了 Go 标准库处理常见数据结构的能力,使得代码更简洁、类型安全且可复用。
可以对自定义 struct 使用这些标准库工具吗?
可以,但有条件,并且通常需要借助 Func 结尾的函数版本。
comparable 约束的函数map 的键(key),也可以用在 slices.Contains、slices.Index、maps.Keys (如果用作 map key)、maps.Clone (如果用作 key 或 value)等需要 comparable 约束的地方。package main
import (
"fmt"
"maps"
"slices"
)
// Person struct - all fields (string, int) are comparable
type Person struct {
Name string
Age int
}
func main() {
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Name: "Bob", Age: 25}
p3 := Person{Name: "Alice", Age: 30} // Same as p1
people := []Person{p1, p2}
// 1. 使用 slices.Contains (需要 comparable)
fmt.Println("Contains p1?", slices.Contains(people, p1)) // Output: true
fmt.Println("Contains p3?", slices.Contains(people, p3)) // Output: true (value equality)
fmt.Println("Contains {Carol, 40}?", slices.Contains(people, Person{Name: "Carol", Age: 40})) // Output: false
// 2. 用作 map 的 key (需要 comparable)
scores := map[Person]int{
p1: 100,
p2: 95,
}
fmt.Println("Score for p1:", scores[p1]) // Output: 100
keys := maps.Keys(scores)
fmt.Println("Map keys:", keys) // Output: Map keys: [{Alice 30} {Bob 25}] (顺序不定)
}cmp.Ordered 约束的函数cmp.Ordered 约束,因为 Go 不知道如何直接比较 (<, >) 两个结构体的大小。slices.Sort,也不能直接用 cmp.Compare 或 cmp.Less 来比较两个结构体实例。// import "slices"
// people := []Person{p1, p2}
// slices.Sort(people) // !!! 编译错误:Person does not satisfy cmp.OrderedFunc 结尾的函数版本slices 和 maps 包提供了带有 Func 后缀的函数版本,例如 slices.SortFunc、slices.BinarySearchFunc、slices.EqualFunc、maps.EqualFunc 等。示例:使用 slices.SortFunc 对自定义结构体排序
package main
import (
"cmp" // 需要用 cmp.Compare 来辅助比较字段
"fmt"
"slices"
)
type Person struct {
Name string
Age int
}
func main() {
people := []Person{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
{Name: "Charlie", Age: 30},
}
// 定义比较函数:先按年龄升序,年龄相同则按姓名升序
comparePeople := func(a, b Person) int {
// 比较年龄
if diff := cmp.Compare(a.Age, b.Age); diff != 0 {
return diff // 年龄不同,直接返回年龄比较结果 (-1 or 1)
}
// 年龄相同,比较姓名 (姓名是 string,满足 cmp.Ordered)
return cmp.Compare(a.Name, b.Name)
}
// 使用自定义比较函数进行排序
slices.SortFunc(people, comparePeople)
fmt.Println("Sorted people:", people)
// Output: Sorted people: [{Bob 25} {Alice 30} {Charlie 30}]
}总结
comparable 的(所有字段都可比较),那么可以直接用于需要 comparable 约束的 slices 和 maps 函数,以及作为 map 的键。cmp.Ordered) 或自定义相等性判断的情况,自定义结构体不能直接使用 slices.Sort, cmp.Compare 等函数, 必须 使用对应的 Func 版本(如 slices.SortFunc),并提供一个 自定义的比较函数 来告诉标准库如何比较你的结构体实例。这种设计使得这些泛型工具包既能方便地处理内置类型,也具有足够的灵活性来适应用户定义的复杂类型。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。