给内置类型排序
slices.Sort 可以给任何元素类型有序的切片排序:整数、浮点数和字符串。它原地排序,不返回任何值。
输出:
[3 7 19 42 88]
[Bob Carl alice lisa]
true
2 true
字符串按字节排序,所以所有大写 ASCII 字母都排在小写字母前面。给名字排序时,这很少是用户想要的结果。下一节会解决这个问题。
slices 包在 Go 1.21 中加入。它的 Sort 是 pattern-defeating quicksort:O(n log n)、原地排序,而且不稳定。
用 SortFunc 自定义顺序
slices.SortFunc 接收一个比较函数 func(a, b T) int。a 应该排在前面时返回负数,b 应该排在前面时返回正数,两者相等时返回零。对有序类型,辅助函数 cmp.Compare 返回的正是这样的值。
在比较函数里调用 strings.ToLower,只要输入中有大写字母就会分配一个新字符串,而比较大约要执行 n log n 次。切片很大时,先一次性算好小写形式的键。对其他语言的名字(重音符号、地区规则),用 golang.org/x/text/collate,它不在标准库里。
比较函数必须前后一致:如果它说 a 在 b 前面,就必须也说 b 在 a 后面。违反这一点的函数(例如两个值只要不同就返回 -1)会得到顺序错误的切片,而且不会报任何错。整数相减(return a - b)看起来很简洁,但值很大时会溢出;请用 cmp.Compare。
给结构体排序
比较函数拿到的是元素本身,所以按某个字段给结构体排序也是同一个调用:
这里工资相同的员工可能以任意顺序输出,因为 SortFunc 不是稳定排序。如果需要确定的顺序,要么用更多字段打破平局,要么用稳定排序。
按多个字段排序
先比较最重要的字段,只有平局时才比较下一个。cmp.Or(Go 1.22)返回它的第一个非零参数,这样一行就能写完:
输出:
eng 150 Cy
eng 120 Ana
eng 120 Eve
sales 90 Bob
sales 90 Dee
即使第一个比较已经决定了结果,所有比较也都会执行,因为它们只是普通的参数。对字段比较来说代价很小。如果某个打破平局的比较开销很大,就手写 if c := ...; c != 0 { return c } 这样的链。
稳定排序
稳定排序会让比较结果相等的元素保持原来的顺序。当输入本身已经有意义的顺序时这一点很重要,例如按时间排好的记录,现在要按用户分组。
这会打印 [{ana 2} {ana 4} {bob 1} {bob 3} {bob 5}]:每个用户内部,原来的顺序保留了下来。稳定排序要做更多工作,所以只在相等元素的顺序有意义时才用它。
给 map 排序
map 没有顺序。要按键的顺序显示 map,就给它的键排序:slices.Sorted(maps.Keys(m))(Go 1.23)。要按值排序,就用一个查找值的比较函数给键排序:
按单词打破平局很重要:没有它,chan 和 slice(都是 7)每次运行的输出顺序都可能不同,因为键从 map 中取出的顺序是随机的。更多内容见 map。
sort 包:sort.Slice 及其他函数
Go 1.21 之前,排序都通过 sort 包完成。你会在大量现有代码里看到它:
sort.Ints(nums)
sort.Strings(names)
sort.Slice(staff, func(i, j int) bool {
return staff[i].Salary < staff[j].Salary
})
sort.SliceStable(staff, func(i, j int) bool { ... })
值得了解的区别:
sort.Slice | slices.SortFunc | |
|---|---|---|
| 函数接收 | 下标 i, j | 元素 a, b |
| 返回 | bool(i 是否小于 j) | int(负数、零、正数) |
| 类型安全 | 接收 any,使用反射 | 泛型,编译时检查 |
| 速度 | 较慢 | 较快 |
sort.Slice 的一个常见 bug 是闭包捕获了另一个切片,而不是正在排序的那个,因为 less 函数是按位置取下标的。SortFunc 不会有这个 bug,因为它直接把元素交给你。
sort.Interface 类型(Len、Less、Swap)是最早的形式。对于不是单个切片的数据,例如两个必须一起移动的平行切片,它仍然是排序的方法。从 Go 1.22 起,sort.Ints、sort.Strings 和 sort.Float64s 内部直接调用 slices.Sort。
常见错误
- 以为
Sort会返回排好序的切片。 它原地排序,不返回任何值。想要一个新的有序切片,用slices.Sorted(slices.Values(s)),或者先slices.Clone。 - 以为相等的元素会保持原来的顺序。 只有
Stable系列函数保证这一点。 - 用减法做比较。
a - b会溢出。请用cmp.Compare。 - 给含 NaN 的浮点数排序。
cmp.Compare把 NaN 排在所有其他值之前,让排序保持一致。手写的a < b比较做不到。
常见问题
Go 中如何给切片排序?
数字和字符串直接调用 slices.Sort(s)(Go 1.21)。它会原地按升序排序。其他类型或其他顺序用 slices.SortFunc(s, func(a, b T) int { ... }):a 应排在前面时函数返回负数,b 应排在前面时返回正数,两者相等时返回零。
Go 中如何把切片按降序排序?
在比较函数里交换参数:slices.SortFunc(s, func(a, b int) int { return cmp.Compare(b, a) })。也可以先升序排序,再调用 slices.Reverse(s)。
Go 中如何按多个字段给结构体切片排序?
先比较第一个字段,只有相等时才继续比较下一个字段。cmp.Or(Go 1.22)正好做这件事:return cmp.Or(cmp.Compare(a.Dept, b.Dept), cmp.Compare(b.Salary, a.Salary), strings.Compare(a.Name, b.Name)) 返回第一个非零结果。
sort.Slice 和 slices.SortFunc 有什么区别?
sort.Slice(s, func(i, j int) bool) 是较老的 API:它接收一个基于下标的 less 函数,并用反射交换元素。slices.SortFunc(s, func(a, b T) int) 是泛型的,有类型检查,直接拿到元素,而且更快。新代码应该用 slices 包;sort.Slice 在 Go 1.21 之前写的代码里仍然很常见。