登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

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 对 NaN 与普通数返回负一零正一的静态排序结构图
图1:静态说明图,cmp.Compare 把 NaN 放在所有非 NaN 数之前,并把两个 NaN 视为相等。

先记住返回值代表什么

cmp.Compare(x, y) 只返回三种结果:x 小于 y 时为 -1,x 与 y 相等时为 0,x 大于 y 时为 +1。对整数和字符串,这与直觉一致;浮点数出现 NaN 时,则应用标准库的特殊规则。

比较表达式返回值含义
cmp.Compare(NaN, 3.14)-1NaN 被放在非 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
原生浮点运算与 cmp.Compare 处理 NaN 和正负零差异的静态关系图
图2:静态关系图,原生运算保留 IEEE 浮点语义,cmp.Compare 则提供排序需要的一致顺序。

所以,不要把返回 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,因此排序比较器会把它们视为同一比较组。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>