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 mainimport ( "fmt" "sort" ) func main () { nums := []int {5 , 2 , 8 , 1 , 9 , 3 , 7 } sort.Ints(nums) fmt.Println(nums) }
就这么简单。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)
3.3 排序字符串切片
1 2 3 4 names := []string {"Charlie" , "Alice" , "Bob" , "David" } sort.Strings(names) fmt.Println(names)
字符串按字典序(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) fmt.Println(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) fmt.Println(sorted)
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 } sort.Slice(nums, func (i, j int ) bool { return nums[i] < nums[j] }) fmt.Println(nums) sort.Slice(nums, func (i, j int ) bool { return nums[i] > nums[j] }) fmt.Println(nums)
less 函数接收两个索引 i 和 j,返回 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) }
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) }
注意 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 }, {"任务D" , 1 , 4 }, } 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) } tasks = []Task{ {"任务A" , 2 , 1 }, {"任务B" , 1 , 2 }, {"任务C" , 2 , 3 }, {"任务D" , 1 , 4 }, } 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) }
什么时候用 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 } type ByAge []Personfunc (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) } }
思路 :定义一个新类型 ByAge(底层是 []Person),在这个类型上实现排序需要的三个方法。排序时把 []Person 转换为 ByAge。
5.3 多种排序方式
用不同类型的别名,可以轻松实现多种排序方式:
1 2 3 4 5 6 7 8 9 type ByName []Personfunc (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 } idx := sort.SearchInts(nums, 7 ) fmt.Println(idx) idx = sort.SearchInts(nums, 6 ) fmt.Println(idx)
注意 :SearchInts 假设切片已经是排序好的。如果没排序,结果无意义。
6.2 sort.SearchStrings 和 sort.SearchFloat64s
1 2 3 4 5 6 names := []string {"Alice" , "Bob" , "Charlie" , "David" } idx := sort.SearchStrings(names, "Charlie" ) fmt.Println(idx) idx = sort.SearchStrings(names, "Cathy" ) fmt.Println(idx)
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 } idx := sort.Search(len (nums), func (i int ) bool { return nums[i] >= 6 }) fmt.Println(idx) fmt.Println(nums[idx]) idx = sort.Search(len (nums), func (i int ) bool { return nums[i] > 5 }) fmt.Println(idx) fmt.Println(nums[idx])
6.4 用二分查找做范围查询
1 2 3 4 5 6 7 8 9 10 11 12 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])
6.5 sort.IsSorted:判断是否已排序
1 2 3 4 5 nums := []int {1 , 2 , 3 , 5 , 4 } fmt.Println(sort.IsSorted(sort.IntSlice(nums))) nums = []int {1 , 2 , 3 , 4 , 5 } fmt.Println(sort.IsSorted(sort.IntSlice(nums)))
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) }
方法二:用 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 { 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) }
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) }
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) }
7.5 判断元素是否存在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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)) fmt.Println(isSubset(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) big := filter(nums, func (n int ) bool { return n > 5 }) fmt.Println(big) }
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 } doubled := mapFunc(nums, func (n int ) int { return n * 2 }) fmt.Println(doubled) squared := mapFunc(nums, func (n int ) int { return n * n }) fmt.Println(squared) }
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) product := reduce(nums, 1 , func (acc, n int ) int { return acc * n }) fmt.Println(product) max := reduce(nums, nums[0 ], func (acc, n int ) int { if n > acc { return n } return acc }) fmt.Println(max) }
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 mainimport ( "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 , "图书" }, } fmt.Println("=== 按价格升序 ===" ) sort.Slice(products, func (i, j int ) bool { return products[i].Price < products[j].Price }) printProducts(products) fmt.Println("=== 按销量降序 ===" ) sort.Slice(products, func (i, j int ) bool { return products[i].Sales > products[j].Sales }) printProducts(products) 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) 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 mainimport ( "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 mainimport ( "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.Slice 的 less 函数不要修改切片
1 2 3 4 5 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)
SearchInts、SearchStrings、Search 都假设切片已排序。没排序的切片上使用会得到无意义的结果。
9.3 二分查找找不到时的返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 nums := []int {1 , 3 , 5 , 7 , 9 } idx := sort.SearchInts(nums, 6 ) fmt.Println(idx) 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.Slice 的 less 函数不能有副作用
1 2 3 4 5 6 7 8 9 count := 0 sort.Slice(nums, func (i, j int ) bool { count++ fmt.Println("第" , count, "次比较" ) return nums[i] < nums[j] })
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 }, } seen := make (map [Person]bool ) unique := []Person{} for _, p := range people { if !seen[p] { seen[p] = true unique = append (unique, p) } } fmt.Println(unique)
结构体可以做 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 和任何数比较(包括自己)都是 false,所以包含 NaN 的切片排序结果不确定。如果有 NaN,需要特殊处理。
9.8 对 nil 切片排序
1 2 3 var nums []int sort.Ints(nums) fmt.Println(nums)
对 nil 切片或空切片排序是安全的,不会 panic。
10. 本课练习
练习 1:学生成绩排序
要求:
定义 Student 结构体(Name、Score、Class)
按成绩降序排列输出
成绩相同的按姓名升序排列
练习 2:商品筛选与排序
要求:
定义 Product 结构体(Name、Price、Category、InStock)
筛选出库存中的商品
按类别分组,每个类别内按价格升序排列
练习 3:集合操作库
要求:
实现 intersect(交集)、union(并集)、difference(差集)
支持 int 类型
返回的结果中不包含重复元素
练习 4:日志分析
要求:
给定一组日志条目(Timestamp、Level、Message)
按时间排序
统计各日志级别的数量
找出出现次数最多的错误消息
练习 5:实现排序算法
要求:
不用 sort 包,自己实现冒泡排序
用 sort.Slice 实现同样的排序
比较两种方式的代码量差异
11. 自测题
11.1 概念题
sort.Slice 和 sort.SliceStable 有什么区别?
sort.Sort 需要实现哪三个方法?
sort.SearchInts 返回 6 但切片中没有 6,这是什么意思?
对 nil 切片调用 sort.Ints 会 panic 吗?
sort.Reverse 的工作原理是什么?
在 sort.Slice 的比较函数中修改切片数据会怎样?
用 map 去重时,如何保持元素的原始顺序?
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) }
点击查看答案
解释:
按年龄升序排列
Alice、Bob、David 年龄都是 25,Charlie 是 30
因为用的是 SliceStable,所以同年龄的人保持原来的顺序:Alice → Bob → David
Charlie 年龄最大,排在最后
12. 本课总结
这一课你学到了 Go 中排序和集合操作的方法。
场景
推荐方式
基本类型排序
sort.Ints / sort.Float64s / sort.Strings
按自定义规则排序
sort.Slice(最常用)
保持相等元素顺序
sort.SliceStable
降序排序
sort.Reverse 包装,或 less 中反转比较
多字段排序
less 函数中先比主字段,再比次字段
二分查找
sort.SearchInts / sort.Search(必须已排序)
去重
map 辅助
交集/并集/差集
map 辅助
过滤
遍历 + 条件判断
最重要的三件事:
排序用 sort.Slice,简单直接——大多数情况不需要 sort.Interface
二分查找必须在已排序的切片上使用——没排序就用会得到错误结果
Go 没有内置集合,用 map 来实现——map[string]bool 是最常见的"集合"
13. 下一课预告
下一课是阶段四的最后一课:命令行小项目实战 。
会重点讲:
整合前面所学(结构体、切片、map、文件、JSON、排序、错误处理)
完成一个具有实际功能的 CLI 程序
项目的组织与代码结构
学完下一课,你就完成了阶段四,具备了用 Go 做真实小项目的完整能力。