Go cmp.Compare 遇到 NaN 时结果怎么理解
来源:17golang原创
时间:2026-10-04 04:34:11 258浏览 收藏
直接结论:Go 的 cmp.Compare 为 NaN 规定了一套可用于排序的一致规则:NaN 小于任何非 NaN 浮点数;NaN 与 NaN 比较时返回 0;非 NaN 与 NaN 比较时返回 +1。它还把负零和正零视为相等。
官方文档:https://pkg.go.dev/cmp
因此,若直接把 cmp.Compare 交给升序排序,所有 NaN 会排在普通数值之前。这不是在说 NaN 具有通常数学意义上的大小,而是标准库为了让比较函数能够稳定工作而定义的排序语义。

先记住返回值代表什么
cmp.Compare(x, y) 只返回三种结果:x 小于 y 时为 -1,x 与 y 相等时为 0,x 大于 y 时为 +1。对整数和字符串,这与直觉一致;浮点数出现 NaN 时,则应用标准库的特殊规则。
| 比较表达式 | 返回值 | 含义 |
|---|---|---|
| cmp.Compare(NaN, 3.14) | -1 | NaN 被放在非 NaN 之前 |
| cmp.Compare(3.14, NaN) | +1 | 非 NaN 被放在 NaN 之后 |
| cmp.Compare(NaN, NaN) | 0 | 两个 NaN 在该比较规则下相等 |
| cmp.Compare(-0.0, +0.0) | 0 | 负零与正零被视为相等 |
用最小示例核对三条 NaN 规则
下面把三个关键组合一次打印出来。输出中的 -1、0、+1 是比较结果,不是参与比较的浮点值。
package main
import (
"cmp"
"fmt"
"math"
)
func main() {
nan := math.NaN()
value := 3.14
// 分别核对 NaN 在左侧、右侧和两侧时的返回值
fmt.Println(cmp.Compare(nan, value))
fmt.Println(cmp.Compare(value, nan))
fmt.Println(cmp.Compare(nan, nan))
}
-1
1
0
第一行说明 NaN 在升序中靠前;第二行是相反方向;第三行保证两个 NaN 能进入同一个“相等分组”。这使比较函数可以满足排序算法需要的一致性。
为什么和原生等号、小于号看起来矛盾
原生浮点运算遵循 NaN 的特殊性质:NaN 与任何值做等号比较都不会得到 true,包括它自己;用小于号比较也不会得到普通的小于结论。cmp.Compare 的任务不同,它需要返回一个明确的三态结果,所以专门补上了稳定顺序。
package main
import (
"cmp"
"fmt"
"math"
)
func main() {
nan := math.NaN()
// 原生相等判断与 cmp.Compare 的排序相等不是同一语义
fmt.Println(nan == nan)
fmt.Println(nan
false
false
true

所以,不要把返回 0 解释为“两个 NaN 的二进制内容完全一致”,也不要据此替换业务中的 NaN 检测。要判断一个值是不是 NaN,仍应使用 math.IsNaN。
负零和正零为什么也返回 0
浮点数可以保存符号不同的零。cmp.Compare 明确把 -0.0 与 +0.0 视为相等,因此返回 0。若业务必须保留零的符号,例如某些数值分析或序列化场景,应另外检查符号位,而不能只看比较结果。
package main
import (
"cmp"
"fmt"
"math"
)
func main() {
negativeZero := math.Copysign(0, -1)
positiveZero := 0.0
// Compare 视两种零为相等,Signbit 仍能识别符号
fmt.Println(cmp.Compare(negativeZero, positiveZero))
fmt.Println(math.Signbit(negativeZero), math.Signbit(positiveZero))
}
0
true false
排序时 NaN 默认靠前
把 cmp.Compare 直接传给切片排序,升序结果会先出现 NaN,再出现从小到大的普通数值。这个默认规则适合希望异常值集中在开头、便于优先检查的数据清洗流程。
package main
import (
"cmp"
"fmt"
"math"
"slices"
)
func main() {
values := []float64{8, math.NaN(), -2, 3}
// cmp.Compare 的升序规则会把 NaN 排在普通数值之前
slices.SortFunc(values, cmp.Compare[float64])
fmt.Println(values)
}
[NaN -2 3 8]
业务要求 NaN 靠后时要显式改规则
报表、排行榜或图表经常希望有效数字优先展示,把缺失值放到末尾。这时不要误以为交换升降序就能解决,因为那也会反转普通数字的顺序。更清楚的做法是先处理 NaN,再把普通数交给 cmp.Compare。
package main
import (
"cmp"
"fmt"
"math"
"slices"
)
func compareNaNLast(a, b float64) int {
// 两边都是 NaN 时归为同一组
if math.IsNaN(a) && math.IsNaN(b) {
return 0
}
// 只有左边是 NaN 时,让左边排到后面
if math.IsNaN(a) {
return 1
}
// 只有右边是 NaN 时,让左边普通数排在前面
if math.IsNaN(b) {
return -1
}
return cmp.Compare(a, b)
}
func main() {
values := []float64{8, math.NaN(), -2, 3}
slices.SortFunc(values, compareNaNLast)
fmt.Println(values)
}
[-2 3 8 NaN]
判断清单
- 看到 -1:左值排在右值之前;NaN 对非 NaN 就是这一结果。
- 看到 0:在 cmp.Compare 的排序语义下等价,不代表 NaN 原生等号为 true。
- 看到 +1:左值排在右值之后;非 NaN 对 NaN 就是这一结果。
- 直接升序排序:NaN 靠前,普通数按正常大小排列。
- 业务要 NaN 靠后:先用 math.IsNaN 分组,再比较普通数。
- 业务关心零的符号:另外使用 math.Signbit,不依赖 Compare 区分。
总结:把 cmp.Compare 看成“为排序建立确定关系”的工具,就不会和原生浮点运算混淆。它把 NaN 放在普通数之前、把两个 NaN 视为相等,并把正负零视为相等;若业务排序不同,就在比较函数里显式调整。
相关问题
cmp.Less 对 NaN 的规则一样吗?
方向一致:NaN 小于任何非 NaN,而非 NaN 不小于 NaN。两个 NaN 之间不会报告小于。
可以用 cmp.Compare 判断一个值是不是 NaN 吗?
不建议。判断单个值是否为 NaN 应使用 math.IsNaN,语义更直接。
多个不同 NaN 会在排序中放在一起吗?
会。cmp.Compare 将 NaN 与 NaN 的比较结果定义为 0,因此排序比较器会把它们视为同一比较组。
-
335 收藏
-
372 收藏
-
370 收藏
-
141 收藏
-
417 收藏
-
145 收藏
-
436 收藏
-
354 收藏
-
210 收藏
-
146 收藏
-
145 收藏
-
384 收藏
-
414 收藏
-
135 收藏
-
124 收藏
-
431 收藏
-
245 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习