Go 从 0 到精通 · 第 23 课:排序、集合与常见算法工具

学习定位:这是整套 Go 教程的第 23 课。
前置要求:已经完成第 22 课,掌握了 JSON 编解码。需要熟悉结构体、切片、接口和方法。
本课目标:掌握 Go 中 sort 包的使用,能对各种类型的数据进行排序,理解自定义排序的思路,能用切片和 map 实现常见的集合操作(去重、交集、并集等),能对结构化数据进行灵活的排序处理。


1. 本课你要解决的核心问题

排序和集合操作是编程中最常见的任务。用户列表要按注册时间排序,商品列表要按价格排序,搜索结果要按相关度排序。同时,你经常需要做去重、求交集、过滤等操作。

你需要搞明白以下问题:

  • Go 的排序和其他语言有什么不同
  • 怎么对整数、字符串切片排序
  • 怎么按结构体的某个字段排序
  • 怎么实现多字段排序(先按 A 排,再按 B 排)
  • 怎么做降序排列
  • 怎么判断一个切片是否已排序
  • 怎么在有序切片中查找元素
  • 怎么用切片和 map 实现集合操作

学完这一课,你就能自如地处理数据排序和集合操作了。


2. sort 包总览

Go 的排序功能都在 sort 包里。先看核心概念:

Go 的排序和其他语言最大的不同:Go 没有对泛型排序的内置支持(在 1.21 之前),而是通过接口来实现排序。你需要理解 sort.Interface

2.1 核心类型和函数

函数/类型 用途
sort.Ints([]int) 对 int 切片升序排序
sort.Float64s([]float64) 对 float64 切片升序排序
sort.Strings([]string) 对 string 切片升序排序
sort.Sort(data) 按自定义规则排序(需要实现 sort.Interface
sort.Slice(s, less) 按自定义函数排序(Go 1.8+,最常用)
sort.SliceStable(s, less) 同上,但保持相等元素的原始顺序
sort.Reverse(data) 反转排序顺序
sort.IsSorted(data) 判断是否已排序
sort.Search(n, f) 二分查找

下面逐步展开。


3. 基本类型排序

3.1 排序整数切片

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
"sort"
)

func main() {
nums := []int{5, 2, 8, 1, 9, 3, 7}

sort.Ints(nums)
fmt.Println(nums) // [1 2 3 5 7 8 9]
}

就这么简单。sort.Ints 直接修改原切片,升序排列。

3.2 排序浮点数切片

1
2
3
4
prices := []float64{29.9, 9.9, 59.9, 19.9, 39.9}

sort.Float64s(prices)
fmt.Println(prices) // [9.9 19.9 29.9 39.9 59.9]

3.3 排序字符串切片

1
2
3
4
names := []string{"Charlie", "Alice", "Bob", "David"}

sort.Strings(names)
fmt.Println(names) // [Alice Bob Charlie David]

字符串按字典序(Unicode 编码值)排序。

1
2
3
4
// 中文排序
cities := []string{"北京", "上海", "广州", "深圳"}
sort.Strings(cities)
fmt.Println(cities) // [北京 广州 上海 深圳](按拼音首字母排序)

注意:sort.Strings 对中文按 Unicode 编码值排序,结果和拼音排序可能不完全一致。如果需要精确的拼音排序,需要第三方库。

3.4 排序是原地的

重要:所有 sort 包的函数都是原地排序,直接修改传入的切片,不会创建新的切片。

1
2
3
4
5
6
7
original := []int{3, 1, 2}
sorted := original

sort.Ints(sorted)
fmt.Println(original) // [1 2 3](也被修改了!)
fmt.Println(sorted) // [1 2 3]
// original 和 sorted 指向同一个底层数组

如果你需要保留原始数据,先拷贝:

1
2
3
4
5
6
7
original := []int{3, 1, 2}
sorted := make([]int, len(original))
copy(sorted, original)

sort.Ints(sorted)
fmt.Println(original) // [3 1 2](不变)
fmt.Println(sorted) // [1 2 3]

4. sort.Slice——最灵活的排序方式

4.1 基本用法

sort.Slice 是 Go 1.8 引入的,它接收一个切片和一个比较函数,是最常用的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
nums := []int{5, 2, 8, 1, 9}

// 升序:i 对应的元素 < j 对应的元素
sort.Slice(nums, func(i, j int) bool {
return nums[i] < nums[j]
})
fmt.Println(nums) // [1 2 5 8 9]

// 降序
sort.Slice(nums, func(i, j int) bool {
return nums[i] > nums[j]
})
fmt.Println(nums) // [9 8 5 2 1]

less 函数接收两个索引 ij,返回 true 表示索引 i 的元素应该排在索引 j 的元素前面。

4.2 按结构体字段排序

这是最常见的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Student struct {
Name string
Score int
Age int
}

students := []Student{
{"Alice", 95, 20},
{"Bob", 88, 22},
{"Charlie", 92, 19},
{"David", 88, 21},
}

// 按分数排序(降序)
sort.Slice(students, func(i, j int) bool {
return students[i].Score > students[j].Score
})

for _, s := range students {
fmt.Printf("%s: %d分\n", s.Name, s.Score)
}
// Alice: 95分
// Charlie: 92分
// Bob: 88分
// David: 88分

4.3 多字段排序

按分数降序排列,分数相同则按年龄升序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sort.Slice(students, func(i, j int) bool {
if students[i].Score != students[j].Score {
return students[i].Score > students[j].Score // 分数降序
}
return students[i].Age < students[j].Age // 年龄升序
})

for _, s := range students {
fmt.Printf("%s: %d分, %d岁\n", s.Name, s.Score, s.Age)
}
// Alice: 95分, 20岁
// Charlie: 92分, 19岁
// David: 88分, 21岁
// Bob: 88分, 22岁

注意 Bob 和 David:分数都是 88,但 David 年龄小(21 < 22),所以排在前面。

4.4 sort.Slice vs sort.SliceStable

sort.Slice 不保证相等元素的相对顺序。sort.SliceStable 保持相等元素的原始顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
type Task struct {
Name string
Priority int
Order int // 原始顺序
}

tasks := []Task{
{"任务A", 2, 1},
{"任务B", 1, 2},
{"任务C", 2, 3}, // 和任务A优先级相同
{"任务D", 1, 4}, // 和任务B优先级相同
}

// 用 sort.Slice(不稳定)
sort.Slice(tasks, func(i, j int) bool {
return tasks[i].Priority < tasks[j].Priority
})
fmt.Println("sort.Slice:")
for _, t := range tasks {
fmt.Printf(" %s (优先级:%d, 原序:%d)\n", t.Name, t.Priority, t.Order)
}
// 任务B 和 任务D 的顺序可能互换
// 任务A 和 任务C 的顺序可能互换

// 重置数据
tasks = []Task{
{"任务A", 2, 1},
{"任务B", 1, 2},
{"任务C", 2, 3},
{"任务D", 1, 4},
}

// 用 sort.SliceStable(稳定)
sort.SliceStable(tasks, func(i, j int) bool {
return tasks[i].Priority < tasks[j].Priority
})
fmt.Println("sort.SliceStable:")
for _, t := range tasks {
fmt.Printf(" %s (优先级:%d, 原序:%d)\n", t.Name, t.Priority, t.Order)
}
// 任务B(原序2) 一定在 任务D(原序4) 前面
// 任务A(原序1) 一定在 任务C(原序3) 前面

什么时候用 SliceStable:当你需要按某个字段排序,但相同字段值的元素要保持原来的顺序时。比如"先按优先级排,同优先级的按创建时间排"。


5. sort.Interface——Go 的排序哲学

5.1 什么是 sort.Interface

1
2
3
4
5
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}

只要你实现了这三个方法,就可以用 sort.Sort 排序。这是 Go 的经典做法——用接口表达能力。

5.2 用 sort.Interface 排序自定义类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type Person struct {
Name string
Age int
}

// 定义一个类型,实现 sort.Interface
type ByAge []Person

func (p ByAge) Len() int { return len(p) }
func (p ByAge) Less(i, j int) bool { return p[i].Age < p[j].Age }
func (p ByAge) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

func main() {
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}

sort.Sort(ByAge(people))

for _, p := range people {
fmt.Printf("%s: %d岁\n", p.Name, p.Age)
}
// Bob: 25岁
// Alice: 30岁
// Charlie: 35岁
}

思路:定义一个新类型 ByAge(底层是 []Person),在这个类型上实现排序需要的三个方法。排序时把 []Person 转换为 ByAge

5.3 多种排序方式

用不同类型的别名,可以轻松实现多种排序方式:

1
2
3
4
5
6
7
8
9
type ByName []Person

func (p ByName) Len() int { return len(p) }
func (p ByName) Less(i, j int) bool { return p[i].Name < p[j].Name }
func (p ByName) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

// 使用
sort.Sort(ByAge(people)) // 按年龄排序
sort.Sort(ByName(people)) // 按姓名排序

每种排序方式定义一个类型,各管各的,互不干扰。

5.4 降序排序

sort.Reverse 包装一个 sort.Interface,反转 Less 的结果:

1
sort.Sort(sort.Reverse(ByAge(people)))  // 按年龄降序

6. 查找操作

6.1 sort.SearchInts:在有序切片中查找

1
2
3
4
5
6
7
8
9
nums := []int{1, 3, 5, 7, 9, 11, 13}

// 查找 7 的位置
idx := sort.SearchInts(nums, 7)
fmt.Println(idx) // 3

// 查找不存在的元素
idx = sort.SearchInts(nums, 6)
fmt.Println(idx) // 3(返回应该插入的位置,即第一个 >= 6 的位置)

注意SearchInts 假设切片已经是排序好的。如果没排序,结果无意义。

6.2 sort.SearchStringssort.SearchFloat64s

1
2
3
4
5
6
names := []string{"Alice", "Bob", "Charlie", "David"}
idx := sort.SearchStrings(names, "Charlie")
fmt.Println(idx) // 2

idx = sort.SearchStrings(names, "Cathy")
fmt.Println(idx) // 2(插入到 Charlie 前面)

6.3 sort.Search:通用二分查找

sort.Search 是更通用的版本,你提供一个判断函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
nums := []int{1, 3, 5, 7, 9, 11, 13}

// 找第一个 >= 6 的数的位置
idx := sort.Search(len(nums), func(i int) bool {
return nums[i] >= 6
})
fmt.Println(idx) // 3
fmt.Println(nums[idx]) // 7

// 找第一个 > 5 的数
idx = sort.Search(len(nums), func(i int) bool {
return nums[i] > 5
})
fmt.Println(idx) // 3
fmt.Println(nums[idx]) // 7

6.4 用二分查找做范围查询

1
2
3
4
5
6
7
8
9
10
11
12
// 找第一个 > 4 且 < 10 的数
nums := []int{1, 3, 5, 7, 9, 11, 13}

start := sort.Search(len(nums), func(i int) bool {
return nums[i] > 4
})
end := sort.Search(len(nums), func(i int) bool {
return nums[i] >= 10
})

fmt.Println(nums[start:end]) // [5 7 9]
// 所有 > 4 且 < 10 的数

6.5 sort.IsSorted:判断是否已排序

1
2
3
4
5
nums := []int{1, 2, 3, 5, 4}
fmt.Println(sort.IsSorted(sort.IntSlice(nums))) // false

nums = []int{1, 2, 3, 4, 5}
fmt.Println(sort.IsSorted(sort.IntSlice(nums))) // true

7. 集合操作——用切片和 map 实现

Go 没有内置的集合类型。但你可以用切片和 map 来实现常见的集合操作。

7.1 去重

方法一:用 map 去重(无序)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func dedup(items []string) []string {
seen := make(map[string]bool)
result := []string{}

for _, item := range items {
if !seen[item] {
seen[item] = true
result = append(result, item)
}
}
return result
}

func main() {
tags := []string{"go", "json", "go", "http", "json", "go"}
unique := dedup(tags)
fmt.Println(unique) // [go json http]
}

方法二:用 map 去重并保持顺序

上面的方法已经保持了顺序(因为是按遍历顺序添加的)。注意遍历 map 的顺序是随机的,但我们这里是按切片顺序遍历的,所以结果是稳定的。

方法三:对整数去重

1
2
3
4
5
6
7
8
9
10
11
func dedupInt(nums []int) []int {
seen := make(map[int]bool)
result := []int{}
for _, n := range nums {
if !seen[n] {
seen[n] = true
result = append(result, n)
}
}
return result
}

7.2 交集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func intersect(a, b []string) []string {
// 用 map 记录 b 中的元素
setB := make(map[string]bool)
for _, item := range b {
setB[item] = true
}

result := []string{}
seen := make(map[string]bool) // 防止结果中出现重复
for _, item := range a {
if setB[item] && !seen[item] {
result = append(result, item)
seen[item] = true
}
}
return result
}

func main() {
a := []string{"go", "java", "python", "go", "rust"}
b := []string{"python", "go", "c++", "javascript"}

common := intersect(a, b)
fmt.Println(common) // [go python]
}

7.3 并集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func union(a, b []string) []string {
seen := make(map[string]bool)
result := []string{}

for _, item := range a {
if !seen[item] {
seen[item] = true
result = append(result, item)
}
}
for _, item := range b {
if !seen[item] {
seen[item] = true
result = append(result, item)
}
}
return result
}

func main() {
a := []string{"go", "java", "python"}
b := []string{"python", "go", "rust"}

all := union(a, b)
fmt.Println(all) // [go java python rust]
}

7.4 差集(A - B)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func difference(a, b []string) []string {
setB := make(map[string]bool)
for _, item := range b {
setB[item] = true
}

result := []string{}
for _, item := range a {
if !setB[item] {
result = append(result, item)
}
}
return result
}

func main() {
a := []string{"go", "java", "python", "rust"}
b := []string{"python", "go"}

diff := difference(a, b)
fmt.Println(diff) // [java rust]
}

7.5 判断元素是否存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 用 map 做快速查找
func contains(items []string, target string) bool {
set := make(map[string]bool)
for _, item := range items {
set[item] = true
}
return set[target]
}

// 如果只需要检查一次,用线性搜索就够了
func containsLinear(items []string, target string) bool {
for _, item := range items {
if item == target {
return true
}
}
return false
}

如果需要多次检查,用 map 更高效(O(1) vs O(n))。如果只检查一次,线性搜索就够了,不用建 map。

7.6 子集判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func isSubset(a, b []string) bool {
setB := make(map[string]bool)
for _, item := range b {
setB[item] = true
}
for _, item := range a {
if !setB[item] {
return false
}
}
return true
}

func main() {
a := []string{"go", "python"}
b := []string{"go", "java", "python", "rust"}

fmt.Println(isSubset(a, b)) // true(a 是 b 的子集)
fmt.Println(isSubset(b, a)) // false(b 不是 a 的子集)
}

7.7 过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 过滤出满足条件的元素
func filter(nums []int, predicate func(int) bool) []int {
result := []int{}
for _, n := range nums {
if predicate(n) {
result = append(result, n)
}
}
return result
}

func main() {
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// 过滤出偶数
evens := filter(nums, func(n int) bool {
return n%2 == 0
})
fmt.Println(evens) // [2 4 6 8 10]

// 过滤出大于5的数
big := filter(nums, func(n int) bool {
return n > 5
})
fmt.Println(big) // [6 7 8 9 10]
}

7.8 映射(map 操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 对每个元素做变换
func mapFunc(nums []int, transform func(int) int) []int {
result := make([]int, len(nums))
for i, n := range nums {
result[i] = transform(n)
}
return result
}

func main() {
nums := []int{1, 2, 3, 4, 5}

// 每个数乘以 2
doubled := mapFunc(nums, func(n int) int {
return n * 2
})
fmt.Println(doubled) // [2 4 6 8 10]

// 每个数的平方
squared := mapFunc(nums, func(n int) int {
return n * n
})
fmt.Println(squared) // [1 4 9 16 25]
}

7.9 归约(reduce)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func reduce(nums []int, initial int, combine func(int, int) int) int {
result := initial
for _, n := range nums {
result = combine(result, n)
}
return result
}

func main() {
nums := []int{1, 2, 3, 4, 5}

// 求和
sum := reduce(nums, 0, func(acc, n int) int {
return acc + n
})
fmt.Println(sum) // 15

// 求积
product := reduce(nums, 1, func(acc, n int) int {
return acc * n
})
fmt.Println(product) // 120

// 求最大值
max := reduce(nums, nums[0], func(acc, n int) int {
if n > acc {
return n
}
return acc
})
fmt.Println(max) // 5
}

8. 实战综合示例

8.1 商品排序与筛选系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package main

import (
"fmt"
"sort"
)

type Product struct {
ID int
Name string
Price float64
Sales int
Rating float64
Category string
}

func main() {
products := []Product{
{1, "Go 编程指南", 59.9, 1200, 4.8, "图书"},
{2, "机械键盘", 299.0, 800, 4.5, "外设"},
{3, "显示器", 1999.0, 300, 4.7, "外设"},
{4, "Python 入门", 49.9, 2000, 4.6, "图书"},
{5, "鼠标", 99.0, 1500, 4.3, "外设"},
{6, "Java 核心技术", 79.9, 900, 4.9, "图书"},
}

// 1. 按价格升序
fmt.Println("=== 按价格升序 ===")
sort.Slice(products, func(i, j int) bool {
return products[i].Price < products[j].Price
})
printProducts(products)

// 2. 按销量降序
fmt.Println("=== 按销量降序 ===")
sort.Slice(products, func(i, j int) bool {
return products[i].Sales > products[j].Sales
})
printProducts(products)

// 3. 按评分降序,评分相同按销量降序
fmt.Println("=== 按评分降序(同分按销量)===")
sort.Slice(products, func(i, j int) bool {
if products[i].Rating != products[j].Rating {
return products[i].Rating > products[j].Rating
}
return products[i].Sales > products[j].Sales
})
printProducts(products)

// 4. 筛选外设类商品,再按价格排序
fmt.Println("=== 外设类商品(按价格)===")
peripherals := filterProducts(products, func(p Product) bool {
return p.Category == "外设"
})
sort.Slice(peripherals, func(i, j int) bool {
return peripherals[i].Price < peripherals[j].Price
})
printProducts(peripherals)
}

func printProducts(products []Product) {
for _, p := range products {
fmt.Printf(" %s: ¥%.1f, 销量:%d, 评分:%.1f\n",
p.Name, p.Price, p.Sales, p.Rating)
}
fmt.Println()
}

func filterProducts(products []Product, predicate func(Product) bool) []Product {
result := []Product{}
for _, p := range products {
if predicate(p) {
result = append(result, p)
}
}
return result
}

8.2 数据去重与统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main

import (
"fmt"
"sort"
)

type LogEntry struct {
Timestamp string
Level string
Message string
}

func main() {
logs := []LogEntry{
{"10:00:01", "INFO", "服务启动"},
{"10:00:02", "INFO", "连接数据库"},
{"10:00:03", "WARN", "数据库响应慢"},
{"10:00:04", "INFO", "服务启动"}, // 重复消息
{"10:00:05", "ERROR", "数据库连接失败"},
{"10:00:06", "INFO", "重试连接"},
{"10:00:07", "WARN", "数据库响应慢"}, // 重复消息
{"10:00:08", "INFO", "连接成功"},
}

// 统计各级别的日志数量
levelCount := make(map[string]int)
for _, log := range logs {
levelCount[log.Level]++
}

// 按数量排序输出
type KV struct {
Key string
Value int
}
var sorted []KV
for k, v := range levelCount {
sorted = append(sorted, KV{k, v})
}
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].Value > sorted[j].Value
})

fmt.Println("=== 日志级别统计 ===")
for _, kv := range sorted {
fmt.Printf(" %s: %d 条\n", kv.Key, kv.Value)
}

// 去重(按消息内容去重)
fmt.Println("\n=== 去重后的日志 ===")
seen := make(map[string]bool)
unique := []LogEntry{}
for _, log := range logs {
key := log.Level + ":" + log.Message
if !seen[key] {
seen[key] = true
unique = append(unique, log)
}
}
for _, log := range unique {
fmt.Printf(" [%s] %s %s\n", log.Timestamp, log.Level, log.Message)
}
}

8.3 成绩排名与分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package main

import (
"fmt"
"sort"
)

type ScoreRecord struct {
Name string
Math int
English int
Chinese int
}

func (s ScoreRecord) Total() int {
return s.Math + s.English + s.Chinese
}

func (s ScoreRecord) Average() float64 {
return float64(s.Total()) / 3.0
}

func main() {
records := []ScoreRecord{
{"Alice", 95, 88, 92},
{"Bob", 78, 95, 85},
{"Charlie", 88, 76, 95},
{"David", 92, 91, 88},
{"Eve", 65, 72, 78},
}

// 按总分降序排列
sort.Slice(records, func(i, j int) bool {
return records[i].Total() > records[j].Total()
})

fmt.Println("=== 总分排名 ===")
for rank, r := range records {
fmt.Printf("第%d名: %s, 总分:%d, 平均分:%.1f\n",
rank+1, r.Name, r.Total(), r.Average())
}

// 找出各科最高分
fmt.Println("\n=== 各科最高分 ===")
bestMath, bestEnglish, bestChinese := 0, 0, 0
mathName, englishName, chineseName := "", "", ""

for _, r := range records {
if r.Math > bestMath {
bestMath = r.Math
mathName = r.Name
}
if r.English > bestEnglish {
bestEnglish = r.English
englishName = r.Name
}
if r.Chinese > bestChinese {
bestChinese = r.Chinese
chineseName = r.Name
}
}

fmt.Printf(" 数学最高: %s (%d分)\n", mathName, bestMath)
fmt.Printf(" 英语最高: %s (%d分)\n", englishName, bestEnglish)
fmt.Printf(" 语文最高: %s (%d分)\n", chineseName, bestChinese)

// 计算各科平均分
fmt.Println("\n=== 各科平均分 ===")
mathSum, englishSum, chineseSum := 0, 0, 0
for _, r := range records {
mathSum += r.Math
englishSum += r.English
chineseSum += r.Chinese
}
n := float64(len(records))
fmt.Printf(" 数学平均: %.1f\n", float64(mathSum)/n)
fmt.Printf(" 英语平均: %.1f\n", float64(englishSum)/n)
fmt.Printf(" 语文平均: %.1f\n", float64(chineseSum)/n)

// 筛选不及格的学生
fmt.Println("\n=== 不及格学生 ===")
for _, r := range records {
failed := []string{}
if r.Math < 60 {
failed = append(failed, "数学")
}
if r.English < 60 {
failed = append(failed, "英语")
}
if r.Chinese < 60 {
failed = append(failed, "语文")
}
if len(failed) > 0 {
fmt.Printf(" %s: %s 不及格\n", r.Name,
fmt.Sprint(failed))
}
}
}

9. 常见坑总结

9.1 sort.Sliceless 函数不要修改切片

1
2
3
4
5
// 错误:在 less 中修改了切片
sort.Slice(nums, func(i, j int) bool {
nums[i] = nums[i] * 2 // 不要在比较函数里修改数据!
return nums[i] < nums[j]
})

less 函数只应该比较,不应该修改数据。排序过程中会多次调用 less,在其中修改数据会导致不可预期的结果。

9.2 二分查找必须在已排序的切片上使用

1
2
3
nums := []int{5, 3, 1, 4, 2}
idx := sort.SearchInts(nums, 3)
fmt.Println(idx) // 4(错误的结果!)

SearchIntsSearchStringsSearch 都假设切片已排序。没排序的切片上使用会得到无意义的结果。

9.3 二分查找找不到时的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
nums := []int{1, 3, 5, 7, 9}

// 查找 6(不存在)
idx := sort.SearchInts(nums, 6)
fmt.Println(idx) // 3(应该插入的位置)
// 不是 -1!返回的是第一个 >= 6 的元素的位置

// 要判断是否真的找到了,需要自己检查
if idx < len(nums) && nums[idx] == 6 {
fmt.Println("找到了")
} else {
fmt.Println("没找到") // 输出这个
}

9.4 整数溢出导致排序错误

1
2
3
4
5
6
7
8
9
10
11
12
// 非常大的数做减法会溢出
nums := []int{2147483647, -2147483648}

// 错误做法:用减法比较(可能溢出)
sort.Slice(nums, func(i, j int) bool {
return nums[i]-nums[j] < 0 // 溢出!结果不可预测
})

// 正确做法:直接比较
sort.Slice(nums, func(i, j int) bool {
return nums[i] < nums[j]
})

9.5 sort.Sliceless 函数不能有副作用

1
2
3
4
5
6
7
8
9
// 错误:在 less 中打印信息
count := 0
sort.Slice(nums, func(i, j int) bool {
count++
fmt.Println("第", count, "次比较") // 副作用!
return nums[i] < nums[j]
})
// 排序过程中 less 会被调用多次,次数不确定
// 不要在 less 中做计数、日志等操作

9.6 map 去重时注意值类型的比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 对结构体切片去重
type Person struct {
Name string
Age int
}

people := []Person{
{"Alice", 25},
{"Bob", 30},
{"Alice", 25}, // 重复
}

// 用 map 去重
seen := make(map[Person]bool) // 用结构体做 map 键是可以的
unique := []Person{}
for _, p := range people {
if !seen[p] {
seen[p] = true
unique = append(unique, p)
}
}
fmt.Println(unique) // [{Alice 25} {Bob 30}]

结构体可以做 map 的键,前提是所有字段都是可比较的类型(不能有 slice、map、function 字段)。

9.7 浮点数排序中的 NaN

1
2
3
nums := []float64{1.0, math.NaN(), 3.0, 2.0}
sort.Float64s(nums)
fmt.Println(nums) // NaN 的位置不确定

NaN 和任何数比较(包括自己)都是 false,所以包含 NaN 的切片排序结果不确定。如果有 NaN,需要特殊处理。

9.8 对 nil 切片排序

1
2
3
var nums []int  // nil 切片
sort.Ints(nums)
fmt.Println(nums) // nil(不会 panic)

对 nil 切片或空切片排序是安全的,不会 panic。


10. 本课练习

练习 1:学生成绩排序

要求:

  • 定义 Student 结构体(NameScoreClass
  • 按成绩降序排列输出
  • 成绩相同的按姓名升序排列

练习 2:商品筛选与排序

要求:

  • 定义 Product 结构体(NamePriceCategoryInStock
  • 筛选出库存中的商品
  • 按类别分组,每个类别内按价格升序排列

练习 3:集合操作库

要求:

  • 实现 intersect(交集)、union(并集)、difference(差集)
  • 支持 int 类型
  • 返回的结果中不包含重复元素

练习 4:日志分析

要求:

  • 给定一组日志条目(TimestampLevelMessage
  • 按时间排序
  • 统计各日志级别的数量
  • 找出出现次数最多的错误消息

练习 5:实现排序算法

要求:

  • 不用 sort 包,自己实现冒泡排序
  • sort.Slice 实现同样的排序
  • 比较两种方式的代码量差异

11. 自测题

11.1 概念题

  1. sort.Slicesort.SliceStable 有什么区别?
  2. sort.Sort 需要实现哪三个方法?
  3. sort.SearchInts 返回 6 但切片中没有 6,这是什么意思?
  4. 对 nil 切片调用 sort.Ints 会 panic 吗?
  5. sort.Reverse 的工作原理是什么?
  6. sort.Slice 的比较函数中修改切片数据会怎样?
  7. 用 map 去重时,如何保持元素的原始顺序?
  8. Go 有没有内置的集合类型?用什么代替?

11.2 代码阅读题

预测以下代码的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Person struct {
Name string
Age int
}

people := []Person{
{"Alice", 25},
{"Bob", 25},
{"Charlie", 30},
{"David", 25},
}

sort.SliceStable(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})

for _, p := range people {
fmt.Println(p.Name)
}
点击查看答案
1
2
3
4
Alice
Bob
David
Charlie

解释:

  1. 按年龄升序排列
  2. Alice、Bob、David 年龄都是 25,Charlie 是 30
  3. 因为用的是 SliceStable,所以同年龄的人保持原来的顺序:Alice → Bob → David
  4. Charlie 年龄最大,排在最后

12. 本课总结

这一课你学到了 Go 中排序和集合操作的方法。

场景 推荐方式
基本类型排序 sort.Ints / sort.Float64s / sort.Strings
按自定义规则排序 sort.Slice(最常用)
保持相等元素顺序 sort.SliceStable
降序排序 sort.Reverse 包装,或 less 中反转比较
多字段排序 less 函数中先比主字段,再比次字段
二分查找 sort.SearchInts / sort.Search(必须已排序)
去重 map 辅助
交集/并集/差集 map 辅助
过滤 遍历 + 条件判断

最重要的三件事:

  1. 排序用 sort.Slice,简单直接——大多数情况不需要 sort.Interface
  2. 二分查找必须在已排序的切片上使用——没排序就用会得到错误结果
  3. Go 没有内置集合,用 map 来实现——map[string]bool 是最常见的"集合"

13. 下一课预告

下一课是阶段四的最后一课:命令行小项目实战

会重点讲:

  • 整合前面所学(结构体、切片、map、文件、JSON、排序、错误处理)
  • 完成一个具有实际功能的 CLI 程序
  • 项目的组织与代码结构

学完下一课,你就完成了阶段四,具备了用 Go 做真实小项目的完整能力。