需要判断两个有序切片谁更小、谁更大时,slices.Compare 可以直接给出字典序结果。它从索引 0 开始逐个比较,遇到第一对不相等元素就停止;如果共同前缀完全相同,则长度更短的切片更小。返回值只有三种:相等为 0,左侧更小为 -1,左侧更大为 +1。
官方地址:https://pkg.go.dev/slices#Compare
把 slices.Compare(a, b) 理解成“按元素从左到右比较的三路结果”即可:先看第一个差异,前缀相同再看长度;不要把它当成排序函数,也不要用它判断两个切片是否包含相同元素但顺序不同。
先把返回值当成三路比较
slices.Compare 的返回值适合直接和 0 比较,而不是依赖具体实现中的数值幅度。官方约定是 0 表示两个切片相等,负数方向统一为 -1,正数方向统一为 +1。
package main
import (
"fmt"
"slices"
)
func main() {
left := []string{"api", "v1", "users"}
right := []string{"api", "v1", "orders"}
// 返回值只表达小于、等于、大于三种关系。
result := slices.Compare(left, right)
switch {
case result < 0:
fmt.Println("left 更小")
case result > 0:
fmt.Println("left 更大")
default:
fmt.Println("两个切片相等")
}
}
上例比较到第三个元素时,"users" 与 "orders" 已经不同,因此返回负值方向。业务代码只需要判断小于、等于或大于 0;不要把返回值当作距离,也不要进一步推断两个字符串差了多少。
首个不相等元素决定整体顺序
字典序的关键是“首个差异优先”。前面的元素即使有很多个,只要它们相等,比较就继续向后移动;一旦遇到不相等元素,后面的内容不会改变这次比较结果。
例如 []int{2, 8, 100} 与 []int{2, 9, 0} 的比较结果由索引 1 的 8 和 9 决定,索引 2 的 100 与 0 不再参与结果判断。这种规则适合比较已经有稳定字段顺序的版本号片段、路径片段或分页游标。
共同前缀相同后再比较长度
如果一方只是另一方的前缀,例如 []string{"go", "tool"} 和 []string{"go", "tool", "fmt"},前两个元素相等,函数才会进入长度判断。此时短切片小于长切片。
package main
import (
"fmt"
"slices"
)
func main() {
short := []string{"go", "tool"}
long := []string{"go", "tool", "fmt"}
// 共同前缀相同,长度更短的一方按字典序更小。
fmt.Println(slices.Compare(short, long)) // -1
fmt.Println(slices.Compare(long, short)) // +1
// 空切片是任意非空切片的前缀,因此小于非空切片。
fmt.Println(slices.Compare([]int{}, []int{1})) // -1
}
这个长度规则也解释了为什么“内容相同但一方多了尾部元素”不能视为相等。若业务要的是集合相等、忽略顺序或忽略重复元素,应该先明确那是另一种问题,不要把 Compare 当作通用相等判断器。
元素类型必须满足 cmp.Ordered
当前 API 签名是 Compare[S ~[]E, E cmp.Ordered]。因此元素必须属于可建立自然顺序的类型,例如整数、浮点数或字符串。结构体、映射和切片本身不能直接作为 E 传给 slices.Compare。
| 数据类型 | 能否直接使用 | 处理建议 |
|---|---|---|
[]int、[]string | 可以 | 直接比较,结果遵循自然顺序 |
[]float64 | 可以 | 注意 NaN 的业务语义,必要时先约定规则 |
[]User | 不可以 | 用字段比较函数改用 CompareFunc |
[][]byte | 不可以 | 明确字节序规则后使用自定义比较 |
nil 与空但非 nil 的切片在字典序上都没有元素;两者比较结果为 0。这与一些需要区分 nil 和空值的 JSON 或业务协议语义不同,序列比较前要先确定是否真的只关心元素顺序。
需要自定义顺序时改用 CompareFunc
当元素是结构体,或者两边元素类型不同,slices.Compare 的自然顺序就不够用了。此时使用 slices.CompareFunc,由回调返回负数、0 或正数,函数仍然按“首个非零结果优先、共同前缀后比较长度”的规则工作。
package main
import (
"fmt"
"slices"
)
type User struct {
Name string
Age int
}
func main() {
left := []User{{Name: "Alice", Age: 30}}
right := []User{{Name: "Bob", Age: 20}}
// 结构体没有自然 Ordered 顺序,先定义按 Name 的比较规则。
result := slices.CompareFunc(left, right, func(a, b User) int {
if a.Name < b.Name {
return -1
}
if a.Name > b.Name {
return 1
}
return 0
})
fmt.Println(result)
}
自定义函数必须保持一致的比较方向:返回 0 表示当前元素等价,负值表示左元素排在前面,正值表示左元素排在后面。如果比较函数前后使用了不同规则,结果就没有稳定的字典序含义。
把比较 API 放在正确的问题里
- 只想知道两个切片的字典序关系:使用
slices.Compare。 - 元素不是
cmp.Ordered,或需要按字段、忽略大小写:使用slices.CompareFunc。 - 只想判断长度和每个位置都相等:使用
slices.Equal,不要把负值或正值当成布尔结果。 - 想把切片重新排列:使用
slices.Sort或slices.SortFunc,比较函数本身不会修改输入。
这样拆分后,调用方只需保存一个明确的决策:默认自然顺序还是业务自定义顺序。比较结果再交给分页、版本排序或边界判断逻辑即可。
常见追问
slices.Compare 会修改原切片吗?
不会。它只读取元素并返回比较结果,不负责排序,也不会改变切片长度或底层数组。
为什么两个空切片比较结果是 0?
两者没有不相等元素,长度也相同,所以字典序相等;nil 与空但非 nil 的切片也都没有元素。
比较结构体切片应该怎么办?
为结构体字段定义稳定的比较规则,使用 slices.CompareFunc。如果还要处理相同主键,可以在回调中加入明确的次级字段规则。
参考资料
https://pkg.go.dev/slices#Compare:Go 官方 slices.Compare 文档与示例。https://pkg.go.dev/slices#CompareFunc:Go 官方自定义比较函数文档。