Go 1.21 集合操作三剑客:cmp、slices 与 maps 标准库实战

深入探索 Go 1.21 引入的 cmp、slices、maps 三个标准库包,掌握基于 cmp.Ordered 约束的集合操作,告别手写循环

Go 1.21 集合操作三剑客:cmp、slices 与 maps 标准库实战

如果你写过足够多的 Go 代码,那么下面这些场景你一定不陌生:

// 检查 slice 是否包含某元素 —— 又是一个 5 行的 for 循环
found := false
for _, v := range names {
    if v == "Alice" {
        found = true
        break
    }
}

// 获取 map 的所有 key —— 又是一个 5 行的 for 循环
keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}

// 求 slice 的最小值 —— 又是一个 5 行的 for 循环
min := numbers[0]
for _, v := range numbers[1:] {
    if v < min {
        min = v
    }
}

这些"样板代码"就像每个 Go 开发者都必须缴纳的"语法税"。每交一次,代码就啰嗦一点;每交一次,项目里就多出一堆长得几乎一模一样的辅助函数。

Go 1.18 引入泛型后,社区曾经寄望于 golang.org/x/exp/slicesgolang.org/x/exp/maps 这些实验包。但这些包毕竟不是标准库,引入它们意味着多一份依赖、多一份不确定性。

直到 Go 1.21,这三个包终于"转正"进入标准库:cmpslicesmaps。它们彼此配合,形成了一套完整的集合操作工具箱。从此,你可以用一行代码完成曾经需要十行代码的工作,而且这些代码都是类型安全、性能优化的。

本文将从 cmp 包这个"地基"开始,逐层深入到 slicesmaps 的每个函数,配合大量实战示例,带你真正掌握这三个包。

一、cmp 包:一切的地基

很多人学 slicesmaps 包时直接上手用函数,却忽略了 cmp 包这个地基。但如果你不理解 cmp.Ordered,就很难真正理解 slices.Sortslices.Min 这些函数为什么能工作、为什么对某些类型又会报错。

1.1 为什么需要 cmp 包?

在泛型的世界里,一个核心问题是:怎么表达"可以比较大小"的类型?

比如我想写一个泛型的 Min 函数:

// 这样写是不行的——不是所有类型都支持 < 运算符
func Min[T any](a, b T) T {
    if a < b {  // ❌ 编译错误:invalid operation: a < b (type parameter T is not comparable with <)
        return a
    }
    return b
}

any(也就是 interface{})太宽泛了,它包含所有类型,但 struct{ Name string } 这样的类型显然不支持 < 运算符。编译器无法在编译时确定 T 是否支持比较操作。

我们需要一个更精确的约束,只包含那些"可以比较大小"的类型。这就是 cmp.Ordered 的使命。

1.2 Ordered 约束的定义

cmp.Ordered 的定义非常简洁:

package cmp

type Ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
        ~float32 | ~float64 |
        ~string
}

它包含了所有支持 <<=>=> 运算符的基本类型:

  • 所有整数类型(intint8int16int32int64uint 系列、uintptr
  • 所有浮点数类型(float32float64
  • string 类型

注意 ~ 符号的含义:它表示"底层类型是",所以基于这些类型的自定义类型也能匹配。比如:

type UserID int       // 底层是 int,✅ 满足 Ordered
type Score float64    // 底层是 float64,✅ 满足 Ordered
type Name string      // 底层是 string,✅ 满足 Ordered
type Point struct{}   // ❌ 不满足 Ordered

1.3 写一个你自己的 Ordered 泛型函数

理解了 Ordered,你可以用它来写自己的泛型函数:

package main

import (
    "cmp"
    "fmt"
)

// Min 返回两个有序值中的较小者
func Min[T cmp.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

// Max 返回两个有序值中的较大者
func Max[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// Clamp 把值限制在某个范围内
func Clamp[T cmp.Ordered](value, min, max T) T {
    if value < min {
        return min
    }
    if value > max {
        return max
    }
    return value
}

type Score float64  // 自定义类型,底层是 float64

func main() {
    fmt.Println(Min(3, 5))                // 3
    fmt.Println(Min("apple", "banana"))   // "apple"
    fmt.Println(Max(Score(85.5), Score(92.0))) // 92.0
    fmt.Println(Clamp(15, 0, 10))         // 10
    fmt.Println(Clamp(-5, 0, 10))         // 0
    fmt.Println(Clamp(7, 0, 10))          // 7
}

1.4 cmp.Compare 和 cmp.Less

除了 Ordered 约束,cmp 包还提供了两个实用的比较函数:

package cmp

// Compare 比较两个有序值,返回 -1、0 或 1
func Compare[T Ordered](x, y T) int {
    // x < y → -1
    // x == y → 0
    // x > y → +1
}

// Less 判断 x 是否小于 y
func Less[T Ordered](x, y T) bool {
    return x < y
}

Compare 的返回值约定(-1/0/+1)是一个重要的标准,它被 slices.SortFuncslices.Compare 等函数广泛使用。这种三路比较(three-way comparison)比单纯的 bool 返回值能传递更多信息。

package main

import (
    "cmp"
    "fmt"
)

func main() {
    fmt.Println(cmp.Compare(1, 2))         // -1
    fmt.Println(cmp.Compare(2, 2))         // 0
    fmt.Println(cmp.Compare(3, 2))         // 1
    
    fmt.Println(cmp.Compare("apple", "banana"))  // -1
    fmt.Println(cmp.Compare("banana", "apple"))  // 1
    
    fmt.Println(cmp.Less(1, 2))   // true
    fmt.Println(cmp.Less(2, 1))   // false
}

记住这个 -1/0/+1 的约定,后面写自定义排序函数时会经常用到。

二、slices 包:让切片操作更优雅

slices 包是这三个包中函数最多的,也是日常使用最频繁的。它涵盖了查找、排序、聚合、转换、比较等所有常见的切片操作。

2.1 查找操作

Contains 和 ContainsFunc

检查 slice 是否包含某个元素,可能是使用频率最高的操作:

package main

import (
    "fmt"
    "slices"
)

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    fruits := []string{"apple", "banana", "cherry"}
    
    // 用 == 比较
    fmt.Println(slices.Contains(numbers, 3))          // true
    fmt.Println(slices.Contains(numbers, 10))         // false
    fmt.Println(slices.Contains(fruits, "banana"))    // true
    
    // 复杂结构体需要 ContainsFunc
    type User struct {
        Name string
        Age  int
    }
    users := []User{
        {"Alice", 30},
        {"Bob", 25},
    }
    
    // 检查是否存在名为 Alice 的用户
    hasAlice := slices.ContainsFunc(users, func(u User) bool {
        return u.Name == "Alice"
    })
    fmt.Println(hasAlice) // true
    
    // 检查是否存在未成年人
    hasMinor := slices.ContainsFunc(users, func(u User) bool {
        return u.Age < 18
    })
    fmt.Println(hasMinor) // false
}

为什么需要 ContainsFunc 因为 Contains 使用 == 比较,这对基本类型没问题。但当元素是 struct 或你想按某个条件查找时,就需要 ContainsFunc 提供自定义判断逻辑。

Index 和 IndexFunc

Contains 只告诉你"有没有",Index 还会告诉你"在哪里":

package main

import (
    "fmt"
    "slices"
)

func main() {
    numbers := []int{10, 20, 30, 40, 50}
    
    fmt.Println(slices.Index(numbers, 30)) // 2
    fmt.Println(slices.Index(numbers, 99)) // -1(未找到返回 -1)
    
    type User struct {
        Name string
        Age  int
    }
    users := []User{
        {"Alice", 30},
        {"Bob", 25},
        {"Carol", 35},
    }
    
    // 查找第一个年龄大于 28 的用户
    idx := slices.IndexFunc(users, func(u User) bool {
        return u.Age > 28
    })
    fmt.Println(idx)        // 0
    fmt.Println(users[idx]) // {Alice 30}
}

BinarySearch:已排序切片的快速查找

如果你的 slice 已经排好序,用 BinarySearchContains/Index 快得多(O(log n) vs O(n)):

package main

import (
    "fmt"
    "slices"
)

func main() {
    numbers := []int{1, 3, 5, 7, 9, 11, 13, 15, 17, 19}
    
    // 查找存在的元素
    idx, found := slices.BinarySearch(numbers, 7)
    fmt.Printf("index=%d, found=%v\n", idx, found) // index=3, found=true
    
    // 查找不存在的元素——idx 是它应该插入的位置
    idx, found = slices.BinarySearch(numbers, 8)
    fmt.Printf("index=%d, found=%v\n", idx, found) // index=4, found=false
    
    // 对于自定义类型,使用 BinarySearchFunc
    type User struct {
        ID   int
        Name string
    }
    users := []User{
        {1, "Alice"},
        {2, "Bob"},
        {3, "Carol"},
    }
    
    // 按 ID 查找
    idx, found = slices.BinarySearchFunc(users, 2, func(u User, target int) int {
        return cmp.Compare(u.ID, target)
    })
    fmt.Printf("index=%d, found=%v, user=%v\n", idx, found, users[idx])
    // index=1, found=true, user={2 Bob}
}

性能差距有多大? 在 10 万个元素的 slice 中查找:

  • Index(线性查找):~50 微秒
  • BinarySearch(二分查找):~0.05 微秒

快 1000 倍! 这就是算法的力量。前提是:slice 必须先排序。

2.2 排序操作

Sort:最基本的排序

package main

import (
    "fmt"
    "slices"
)

func main() {
    numbers := []int{5, 2, 8, 1, 9, 3}
    strings := []string{"banana", "apple", "cherry"}
    
    slices.Sort(numbers)
    slices.Sort(strings)
    
    fmt.Println(numbers) // [1 2 3 5 8 9]
    fmt.Println(strings) // [apple banana cherry]
}

Sort 的函数签名是 func Sort[E cmp.Ordered](x []E),注意类型参数 E cmp.Ordered——这正是我们前面讲的 Ordered 约束发挥作用的地方。只有支持 < 比较的类型才能用 Sort

SortFunc:自定义排序规则

当你需要排序的元素不是 cmp.Ordered 类型(比如 struct),或者你想按特定字段排序时:

package main

import (
    "cmp"
    "fmt"
    "slices"
)

type User struct {
    Name string
    Age  int
    City string
}

func main() {
    users := []User{
        {"Alice", 30, "NYC"},
        {"Bob", 25, "LA"},
        {"Carol", 35, "SF"},
        {"David", 28, "NYC"},
    }
    
    // 按年龄升序
    slices.SortFunc(users, func(a, b User) int {
        return cmp.Compare(a.Age, b.Age)
    })
    fmt.Println(users)
    // [{Bob 25 LA} {David 28 NYC} {Alice 30 NYC} {Carol 35 SF}]
    
    // 按年龄降序——翻转比较结果
    slices.SortFunc(users, func(a, b User) int {
        return cmp.Compare(b.Age, a.Age)  // 注意 a 和 b 调换了
    })
    fmt.Println(users)
    // [{Carol 35 SF} {Alice 30 NYC} {David 28 NYC} {Bob 25 LA}]
    
    // 多字段排序:先按 City,再按 Age
    slices.SortFunc(users, func(a, b User) int {
        if c := cmp.Compare(a.City, b.City); c != 0 {
            return c
        }
        return cmp.Compare(a.Age, b.Age)
    })
    fmt.Println(users)
    // [{Bob 25 LA} {David 28 NYC} {Alice 30 NYC} {Carol 35 SF}]
}

重要提醒SortFunc 的比较函数返回 int-1/0/+1),而不是 bool。这与旧版的 sort.Slice 不同。这种设计能正确处理 NaN 等特殊值,也能区分"相等"和"不等"。

SortStableFunc:稳定排序

普通排序不保证相等元素的相对顺序。稳定排序则可以:

package main

import (
    "cmp"
    "fmt"
    "slices"
)

type Event struct {
    Time    int
    Message string
}

func main() {
    events := []Event{
        {10, "first"},
        {5, "second"},
        {10, "third"},   // 和 first 时间相同
        {5, "fourth"},   // 和 second 时间相同
    }
    
    // 稳定排序:相等元素保持原有顺序
    slices.SortStableFunc(events, func(a, b Event) int {
        return cmp.Compare(a.Time, b.Time)
    })
    
    for _, e := range events {
        fmt.Printf("%d: %s\n", e.Time, e.Message)
    }
    // 5: second
    // 5: fourth   (second 仍然在 fourth 前)
    // 10: first
    // 10: third   (first 仍然在 third 前)
}

稳定排序在多字段排序时特别有用——你可以分多次排序,每次排序都不会破坏之前的顺序。

IsSorted 和 IsSortedFunc

检查 slice 是否已经排序:

package main

import (
    "fmt"
    "slices"
)

func main() {
    sorted := []int{1, 2, 3, 4, 5}
    unsorted := []int{1, 3, 2, 4, 5}
    
    fmt.Println(slices.IsSorted(sorted))   // true
    fmt.Println(slices.IsSorted(unsorted)) // false
}

2.3 聚合操作:Min 和 Max

package main

import (
    "fmt"
    "slices"
)

func main() {
    numbers := []int{5, 2, 8, 1, 9, 3}
    words := []string{"banana", "apple", "cherry"}
    
    fmt.Println(slices.Min(numbers)) // 1
    fmt.Println(slices.Max(numbers)) // 9
    fmt.Println(slices.Min(words))   // "apple"(按字典序)
    fmt.Println(slices.Max(words))   // "cherry"
}

MinMax 的类型参数也是 cmp.Ordered。如果你需要对非 Ordered 类型求最值,可以用 MinFuncMaxFunc

package main

import (
    "cmp"
    "fmt"
    "slices"
)

type Product struct {
    Name  string
    Price float64
}

func main() {
    products := []Product{
        {"Laptop", 999.0},
        {"Phone", 599.0},
        {"Tablet", 399.0},
    }
    
    cheapest := slices.MinFunc(products, func(a, b Product) int {
        return cmp.Compare(a.Price, b.Price)
    })
    mostExpensive := slices.MaxFunc(products, func(a, b Product) int {
        return cmp.Compare(a.Price, b.Price)
    })
    
    fmt.Println("最便宜:", cheapest)      // {Tablet 399.0}
    fmt.Println("最贵:", mostExpensive)  // {Laptop 999.0}
}

⚠️ 注意:对空 slice 调用 Min/Max 会 panic。生产代码中建议先检查:

if len(numbers) > 0 {
    fmt.Println(slices.Min(numbers))
}

2.4 转换操作

Compact:去除连续重复元素

Compact 把相邻的相同元素合并成一个。典型用法是配合 Sort 实现去重:

package main

import (
    "fmt"
    "slices"
)

func main() {
    // 去除连续重复
    numbers := []int{1, 1, 2, 2, 2, 3, 3, 4}
    fmt.Println(slices.Compact(numbers)) // [1 2 3 4]
    
    // 非连续的重复不会去除
    letters := []string{"a", "b", "a", "b"}
    fmt.Println(slices.Compact(letters)) // [a b a b](无变化)
    
    // 经典组合:排序 + Compact = 完全去重
    tags := []string{"go", "rust", "go", "python", "rust", "go"}
    slices.Sort(tags)
    unique := slices.Compact(tags)
    fmt.Println(unique) // [go python rust]
}

注意Compact 会修改原 slice 的内容(把重复元素的位置覆盖),返回的 slice 与原 slice 共享底层数组。如果你需要保留原 slice,应该先 Clone

cloned := slices.Clone(original)
compacted := slices.Compact(cloned)

Reverse:反转 slice

package main

import (
    "fmt"
    "slices"
)

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    slices.Reverse(numbers)
    fmt.Println(numbers) // [5 4 3 2 1]
    
    words := []string{"hello", "world", "go"}
    slices.Reverse(words)
    fmt.Println(words) // [go world hello]
}

Reverse 是原地修改,无返回值,不分配新内存。

Replace 和 Insert

package main

import (
    "fmt"
    "slices"
)

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    
    // Replace:替换 [i, j) 区间的元素
    replaced := slices.Replace(numbers, 1, 3, 20, 30)
    fmt.Println(replaced) // [1 20 30 4 5](用 20,30 替换了 2,3)
    
    // 替换时元素数量可以不同
    numbers2 := []int{1, 2, 3, 4, 5}
    replaced2 := slices.Replace(numbers2, 1, 4, 20, 30, 40, 50)
    fmt.Println(replaced2) // [1 20 30 40 50 5]
    
    // Insert:在索引 i 处插入元素
    numbers3 := []int{1, 2, 5}
    inserted := slices.Insert(numbers3, 2, 3, 4)
    fmt.Println(inserted) // [1 2 3 4 5]
}

Delete:删除指定区间的元素

package main

import (
    "fmt"
    "slices"
)

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    deleted := slices.Delete(numbers, 1, 3) // 删除索引 [1, 3)
    fmt.Println(deleted) // [1 4 5]
}

DeleteFunc:按条件删除

package main

import (
    "fmt"
    "slices"
)

func main() {
    numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    
    // 删除所有偶数
    odds := slices.DeleteFunc(numbers, func(n int) bool {
        return n%2 == 0
    })
    fmt.Println(odds) // [1 3 5 7 9]
    
    type User struct {
        Name   string
        Active bool
    }
    users := []User{
        {"Alice", true},
        {"Bob", false},
        {"Carol", true},
        {"David", false},
    }
    
    // 删除不活跃的用户
    activeUsers := slices.DeleteFunc(users, func(u User) bool {
        return !u.Active
    })
    fmt.Println(activeUsers) // [{Alice true} {Carol true}]
}

Clip:释放多余的容量

package main

import (
    "fmt"
    "slices"
)

func main() {
    // 创建一个容量 1000,长度只有 3 的 slice
    numbers := make([]int, 3, 1000)
    numbers[0], numbers[1], numbers[2] = 1, 2, 3
    
    fmt.Printf("before: len=%d, cap=%d\n", len(numbers), cap(numbers))
    // before: len=3, cap=1000
    
    // Clip 把容量裁剪到长度,释放多余内存
    clipped := slices.Clip(numbers)
    fmt.Printf("after: len=%d, cap=%d\n", len(clipped), cap(clipped))
    // after: len=3, cap=3
}

这在使用 DeleteFunc 后特别有用——删除元素后 slice 的容量没变,但实际用不到那么多了,Clip 可以释放这部分内存。

Grow:预扩容

package main

import (
    "fmt"
    "slices"
)

func main() {
    numbers := []int{1, 2, 3}
    
    // 预先扩容至少 100 个元素
    grown := slices.Grow(numbers, 100)
    fmt.Printf("len=%d, cap=%d\n", len(grown), cap(grown))
    // len=3, cap>=103
    
    // 如果原容量已经够大,Grow 不会做多余的事
    big := make([]int, 0, 1000)
    grown2 := slices.Grow(big, 50)
    fmt.Printf("len=%d, cap=%d\n", len(grown2), cap(grown2))
    // len=0, cap=1000(容量未变)
}

当你知道 slice 大概会有多少元素时,用 Grow 预扩容可以避免多次 append 时的内存分配。

2.5 比较操作

Equal 和 EqualFunc

package main

import (
    "fmt"
    "slices"
)

func main() {
    a := []int{1, 2, 3}
    b := []int{1, 2, 3}
    c := []int{1, 2, 4}
    
    fmt.Println(slices.Equal(a, b)) // true
    fmt.Println(slices.Equal(a, c)) // false
    
    // EqualFunc:自定义比较逻辑
    type User struct {
        ID   int
        Name string
    }
    u1 := []User{{1, "Alice"}, {2, "Bob"}}
    u2 := []User{{1, "Alice"}, {2, "Robert"}} // Robert 是 Bob 的正式名
    
    // 只比较 ID
    sameIDs := slices.EqualFunc(u1, u2, func(a, b User) bool {
        return a.ID == b.ID
    })
    fmt.Println(sameIDs) // true
}

Compare 和 CompareFunc

按字典序比较两个 slice:

package main

import (
    "fmt"
    "slices"
)

func main() {
    a := []int{1, 2, 3}
    b := []int{1, 2, 4}
    c := []int{1, 2}
    
    fmt.Println(slices.Compare(a, b)) // -1(a < b)
    fmt.Println(slices.Compare(b, a)) // 1(b > a)
    fmt.Println(slices.Compare(a, a)) // 0(相等)
    fmt.Println(slices.Compare(a, c)) // 1(a 更长,且前缀相同)
}

2.6 Clone

package main

import (
    "fmt"
    "slices"
)

func main() {
    original := []int{1, 2, 3}
    cloned := slices.Clone(original)
    
    // 修改 cloned 不影响 original
    cloned[0] = 100
    fmt.Println(original) // [1 2 3]
    fmt.Println(cloned)   // [100 2 3]
}

Clone 是浅拷贝——对于包含指针的 slice,克隆后的元素仍然指向相同的对象:

type User struct {
    Name string
    Age  int
}

users := []*User{{"Alice", 30}}
cloned := slices.Clone(users)

// 修改指针指向的内容,会影响原 slice
cloned[0].Age = 99
fmt.Println(users[0].Age) // 99 ⚠️

三、maps 包:让字典操作更优雅

maps 包虽然函数不多,但每一个都解决了一个常见的痛点。

3.1 Keys 和 Values:获取所有键和值

package main

import (
    "fmt"
    "maps"
    "slices"
)

func main() {
    ages := map[string]int{
        "Alice": 30,
        "Bob":   25,
        "Carol": 35,
    }
    
    // Keys 返回所有键
    names := maps.Keys(ages)
    fmt.Println(names) // [Alice Bob Carol](顺序不确定)
    
    // Values 返回所有值
    allAges := maps.Values(ages)
    fmt.Println(allAges) // [30 25 35](顺序不确定)
    
    // 组合使用:获取排序后的 key 列表
    sortedNames := maps.Keys(ages)
    slices.Sort(sortedNames)
    fmt.Println(sortedNames) // [Alice Bob Carol]
}

注意KeysValues 返回的 slice 顺序是不确定的,因为 map 的迭代顺序本身就是随机的。如果需要稳定顺序,记得配合 slices.Sort

3.2 Clone 和 Copy

package main

import (
    "fmt"
    "maps"
)

func main() {
    original := map[string]int{
        "Alice": 30,
        "Bob":   25,
    }
    
    // Clone:创建一份独立的副本
    cloned := maps.Clone(original)
    cloned["Carol"] = 35
    fmt.Println(original) // map[Alice:30 Bob:25](原 map 不受影响)
    fmt.Println(cloned)   // map[Alice:30 Bob:25 Carol:35]
    
    // Copy:把一个 map 的内容复制到另一个
    dst := map[string]int{
        "Bob":   99, // 会被覆盖
        "David": 40,
    }
    src := map[string]int{
        "Alice": 30,
        "Bob":   25,
    }
    maps.Copy(dst, src)
    fmt.Println(dst) // map[Alice:30 Bob:25 David:40]
}

Clone vs Copy

  • Clone 创建一个新 map,返回它。
  • Copysrc 的内容合并到已存在的 dst,不返回新 map。遇到相同 key 时,src 覆盖 dst

3.3 DeleteFunc:按条件删除

package main

import (
    "fmt"
    "maps"
)

func main() {
    scores := map[string]int{
        "Alice": 95,
        "Bob":   60,
        "Carol": 88,
        "David": 45,
        "Eve":   72,
    }
    
    // 删除不及格的学生
    maps.DeleteFunc(scores, func(name string, score int) bool {
        return score < 70
    })
    fmt.Println(scores) // map[Alice:95 Carol:88 Eve:72]
}

3.4 Equal 和 EqualFunc

package main

import (
    "fmt"
    "maps"
)

func main() {
    m1 := map[string]int{"a": 1, "b": 2}
    m2 := map[string]int{"a": 1, "b": 2}
    m3 := map[string]int{"a": 1, "b": 3}
    
    fmt.Println(maps.Equal(m1, m2)) // true
    fmt.Println(maps.Equal(m1, m3)) // false
    
    // EqualFunc:自定义比较逻辑
    m4 := map[string]string{"a": "Hello", "b": "World"}
    m5 := map[string]string{"a": "hello", "b": "world"}
    
    // 忽略大小写比较
    same := maps.EqualFunc(m4, m5, func(v1, v2 string) bool {
        return strings.EqualFold(v1, v2)
    })
    fmt.Println(same) // true
}

2.7 迭代器操作(Go 1.23+)

Go 1.23 引入了迭代器(iterator)的概念,slices 包也相应增加了几个处理迭代器的函数。这些函数让你可以用函数式的方式处理数据流。

Collect:从迭代器收集元素到 slice

package main

import (
    "fmt"
    "slices"
)

func main() {
    // All 返回一个迭代器,产生 slice 的所有元素
    numbers := []int{1, 2, 3, 4, 5}
    iter := slices.All(numbers)
    
    // Collect 从迭代器收集所有元素,返回新的 slice
    collected := slices.Collect(iter)
    fmt.Println(collected) // [1 2 3 4 5]
    
    // Values 返回只产生值的迭代器(不包含索引)
    valuesIter := slices.Values(numbers)
    values := slices.Collect(valuesIter)
    fmt.Println(values) // [1 2 3 4 5]
    
    // Backward 返回反向迭代器
    backwardIter := slices.Backward(numbers)
    reversed := slices.Collect(backwardIter)
    fmt.Println(reversed) // [5 4 3 2 1]
}

AppendSeq:将迭代器的元素追加到 slice

package main

import (
    "fmt"
    "slices"
)

func main() {
    // 从多个迭代器追加元素
    result := slices.AppendSeq(
        nil, // 初始 slice
        slices.Values([]int{1, 2, 3}),
        slices.Values([]int{4, 5, 6}),
        slices.Values([]int{7, 8, 9}),
    )
    fmt.Println(result) // [1 2 3 4 5 6 7 8 9]
    
    // 追加到已有的 slice
    existing := []int{10, 20}
    result2 := slices.AppendSeq(
        existing,
        slices.Values([]int{1, 2, 3}),
    )
    fmt.Println(result2) // [10 20 1 2 3]
}

配合 maps.All 使用

迭代器的强大之处在于可以链式组合不同的数据源:

package main

import (
    "fmt"
    "maps"
    "slices"
)

func main() {
    ages := map[string]int{
        "Alice": 30,
        "Bob":   25,
        "Carol": 35,
    }
    
    // maps.All 返回键值对的迭代器
    // 收集所有键
    keys := slices.Collect(maps.Keys(ages))
    fmt.Println(keys) // [Alice Bob Carol](顺序不确定)
    
    // 收集所有值
    values := slices.Collect(maps.Values(ages))
    fmt.Println(values) // [30 25 35](顺序不确定)
    
    // 从多个 map 合并
    m1 := map[string]int{"a": 1, "b": 2}
    m2 := map[string]int{"c": 3, "d": 4}
    
    // 使用 AppendSeq 收集多个 map 的键值对
    type KV struct {
        Key   string
        Value int
    }
    var pairs []KV
    for k, v := range m1 {
        pairs = append(pairs, KV{k, v})
    }
    for k, v := range m2 {
        pairs = append(pairs, KV{k, v})
    }
    fmt.Println(pairs) // [{a 1} {b 2} {c 3} {d 4}](顺序不确定)
}

注意:迭代器是 Go 1.23 引入的特性。如果你的项目使用 Go 1.21 或 1.22,这些函数不可用。但对于新项目,迭代器提供了一种更灵活、更函数式的数据处理方式。

四、性能与最佳实践

4.1 性能对比

这些标准库函数都是经过高度优化的。我们来看几个关键操作的性能:

package main

import (
    "slices"
    "testing"
)

var sink int

func BenchmarkContains(b *testing.B) {
    numbers := make([]int, 100000)
    for i := range numbers {
        numbers[i] = i
    }
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        sink = slices.Index(numbers, 50000)
    }
}

func BenchmarkBinarySearch(b *testing.B) {
    numbers := make([]int, 100000)
    for i := range numbers {
        numbers[i] = i
    }
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        sink, _ = slices.BinarySearch(numbers, 50000)
    }
}

结果(Apple M1):

BenchmarkContains-10       1000000    1100 ns/op
BenchmarkBinarySearch-10   100000000  11 ns/op

二分查找快了 100 倍!这清楚地告诉我们:当数据有序时,永远优先用 BinarySearch

4.2 最佳实践

1. 能用 cmp.Ordered 就用 cmp.Ordered

不要自己重新发明轮子:

// ❌ 不要这样
func MinInt(a, b int) int {
    if a < b { return a }
    return b
}

// ❌ 也不要这样
func MinInterface(a, b interface{}) interface{} { ... }

// ✅ 用 cmp.Ordered
func Min[T cmp.Ordered](a, b T) T {
    if a < b { return a }
    return b
}

// ✅ 更简单:直接用 slices.Min 或内置的 min

2. 排序 + Compact 是去重的标准范式

func Unique[T cmp.Ordered](s []T) []T {
    slices.Sort(s)
    return slices.Compact(s)
}

3. 对大 slice 先排序再查找

// 如果你需要多次查找,先排序
slices.Sort(data)
for _, query := range queries {
    idx, found := slices.BinarySearch(data, query)
    // O(log n) 每次
}

4. 注意浅拷贝的陷阱

CloneCompact 等函数都是浅拷贝。如果 slice 元素包含指针,克隆后的元素仍然共享底层对象。

5. 使用 Clip 释放内存

在对大 slice 执行 DeleteFunc 后,记得用 Clip 释放多余容量:

filtered := slices.DeleteFunc(bigSlice, predicate)
filtered = slices.Clip(filtered) // 释放多余内存

五、实战案例

案例 1:构建一个简单的搜索引擎

package main

import (
    "cmp"
    "fmt"
    "slices"
    "strings"
)

type Document struct {
    ID    int
    Title string
    Score float64
}

func main() {
    docs := []Document{
        {1, "Go 语言入门", 0.95},
        {2, "Python 数据分析", 0.72},
        {3, "Go 并发编程", 0.88},
        {4, "Rust 系统编程", 0.65},
        {5, "Go Web 开发", 0.91},
    }
    
    // 1. 按关键词过滤
    keyword := "Go"
    goDocs := slices.DeleteFunc(slices.Clone(docs), func(d Document) bool {
        return !strings.Contains(d.Title, keyword)
    })
    fmt.Println("包含 'Go' 的文档:")
    for _, d := range goDocs {
        fmt.Printf("  - %s (score: %.2f)\n", d.Title, d.Score)
    }
    
    // 2. 按相关性排序
    slices.SortFunc(goDocs, func(a, b Document) int {
        return cmp.Compare(b.Score, a.Score) // 降序
    })
    fmt.Println("\n按相关性排序:")
    for _, d := range goDocs {
        fmt.Printf("  - %s (%.2f)\n", d.Title, d.Score)
    }
    
    // 3. 获取最高分
    if len(goDocs) > 0 {
        best := slices.MaxFunc(goDocs, func(a, b Document) int {
            return cmp.Compare(a.Score, b.Score)
        })
        fmt.Printf("\n最佳匹配: %s (%.2f)\n", best.Title, best.Score)
    }
}

案例 2:配置合并工具

package main

import (
    "fmt"
    "maps"
)

func main() {
    // 默认配置
    defaultConfig := map[string]string{
        "host":    "localhost",
        "port":    "8080",
        "timeout": "30s",
        "debug":   "false",
    }
    
    // 用户配置(覆盖部分默认值)
    userConfig := map[string]string{
        "port":  "9000",
        "debug": "true",
    }
    
    // 环境特定配置
    envConfig := map[string]string{
        "host": "production.example.com",
    }
    
    // 合并:默认 <- 用户 <- 环境
    finalConfig := maps.Clone(defaultConfig)
    maps.Copy(finalConfig, userConfig)
    maps.Copy(finalConfig, envConfig)
    
    // 删除 debug 标志(生产环境不需要)
    maps.DeleteFunc(finalConfig, func(k, v string) bool {
        return k == "debug"
    })
    
    fmt.Println("最终配置:")
    for k, v := range finalConfig {
        fmt.Printf("  %s: %s\n", k, v)
    }
}

案例 3:数据统计与分析

package main

import (
    "cmp"
    "fmt"
    "maps"
    "slices"
)

type Sale struct {
    Product string
    Amount  float64
    Region  string
}

func main() {
    sales := []Sale{
        {"Laptop", 1200, "North"},
        {"Phone", 800, "South"},
        {"Laptop", 1500, "East"},
        {"Tablet", 400, "North"},
        {"Phone", 900, "East"},
        {"Laptop", 1100, "South"},
        {"Tablet", 500, "East"},
    }
    
    // 1. 按产品汇总销售额
    productTotals := make(map[string]float64)
    for _, s := range sales {
        productTotals[s.Product] += s.Amount
    }
    fmt.Println("产品销售额:")
    for _, product := range slices.Sorted(maps.Keys(productTotals)) {
        fmt.Printf("  %s: $%.2f\n", product, productTotals[product])
    }
    
    // 2. 找出销售额最高的产品
    topProduct := slices.MaxFunc(
        maps.Keys(productTotals),
        func(a, b string) int {
            return cmp.Compare(productTotals[a], productTotals[b])
        },
    )
    fmt.Printf("\n最畅销产品: %s ($%.2f)\n", topProduct, productTotals[topProduct])
    
    // 3. 统计涉及的所有区域
    regions := make([]string, 0, len(sales))
    for _, s := range sales {
        regions = append(regions, s.Region)
    }
    slices.Sort(regions)
    uniqueRegions := slices.Compact(regions)
    fmt.Printf("\n销售区域: %v\n", uniqueRegions)
}

六、从手写循环迁移

如果你正在把项目中的手写循环迁移到这些标准库函数,这里有一份对照表:

slices 操作迁移

手写代码迁移到
for _, v := range s { if v == x { return true } }slices.Contains(s, x)
for i, v := range s { if v == x { return i } }slices.Index(s, x)
sort.Slice(s, ...)slices.Sort(s)slices.SortFunc(s, ...)
for _, v := range s { if v < min { min = v } }slices.Min(s)
for _, v := range s { if v > max { max = v } }slices.Max(s)
for i := 0; i < len(s)/2; i++ { s[i], s[len(s)-1-i] = ... }slices.Reverse(s)
手写去重逻辑slices.Sort(s); s = slices.Compact(s)

maps 操作迁移

手写代码迁移到
keys := make([]K, 0); for k := range m { keys = append(keys, k) }maps.Keys(m)
values := make([]V, 0); for _, v := range m { values = append(values, v) }maps.Values(m)
clone := make(map[K]V); for k, v := range m { clone[k] = v }maps.Clone(m)
for k, v := range src { dst[k] = v }maps.Copy(dst, src)
for k, v := range m { if pred(k, v) { delete(m, k) } }maps.DeleteFunc(m, pred)
手写 map 比较函数maps.Equal(m1, m2)

注意事项

  1. SortFunc 比较函数返回 int 而不是 bool:这是与旧版 sort.Slice 最大的区别。使用 cmp.Compare 是最简单的写法。

  2. KeysValues 返回的 slice 顺序不确定:需要稳定顺序时配合 slices.Sort

  3. Clone 是浅拷贝:对于包含指针、slice、map 的值类型,Clone 后的元素仍然共享底层数据。需要深拷贝时,自己写一个 DeepClone 函数。

小结

Go 1.21 的 cmpslicesmaps 三个包形成了一个完整的集合操作工具箱:

  • cmp 提供基础:Ordered 约束让泛型函数能表达"可比较"的语义,CompareLess 提供标准的比较接口。
  • slices 提供全面:从查找到排序,从聚合到转换,覆盖了切片操作的方方面面。
  • maps 提供专注:虽然函数不多,但每一个都精准解决了 map 操作的常见痛点。

这三个包都基于泛型,类型安全且性能优秀。更重要的是,它们让代码更简洁、更可读、更不容易出错。

从 Go 1.21 开始,当你再次想写一个"检查 slice 是否包含某元素"的 for 循环时,停下来想想:是不是该用 slices.Contains 了?当你又写了一个 for k := range m 来获取所有键时,想想:是不是该用 maps.Keys 了?

告别手写循环,从拥抱这三个包开始。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页