Go:浮点数如何进行比较?

序言

🐒:

   我在学习学习 Go 语言的 数据类型 中的 浮点型 时遇到的不解:浮点数为什么不能直接比较?要比较的话推荐用 差值容限 ?什么是 差值容限 ?我去找 deepseek 问了一下,整理出了这篇文章。


差值容限

🐋:

   在 Go 语言(以及大多数遵循 IEEE 754 标准的编程语言)中,‌差值容限(Epsilon) ‌ 是指在进行浮点数比较时,允许两个数值之间存在的最大微小误差范围。


一、为什么需要差值容限?

🐋:

   计算机使用 二进制 存储浮点数(float32float64),许多十进制小数(如 0.1、0.2 )在二进制中是 无限循环小数,无法被精确表示,只能存储为一个近似值,这导致运算结果会出现微小的精度丢失。


经典案例:

go 复制代码
package main

import "fmt"

func main() {
    f1 := 0.1
    f2 := 0.2
    sum := f1 + f2

    // 数学上 0.1 + 0.2 == 0.3,但在计算机中:
    fmt.Printf("%.20f\n", sum)
    fmt.Println(sum == 0.3)
}
bash 复制代码
0.30000000000000004441
false

直接使用 == 比较会返回 false,因为 sum 实际上比 0.3 大了一点点。为了解决这个问题,我们引入‌ 差值容限‌。


二、差值容限的含义

🐋:

   差值容限 ‌ 是一个极小的正数(通常记为 ϵϵepsilon )。如果两个浮点数 AABB 的差的绝对值 小于这个容限,我们就认为它们在 "工程意义""业务意义" 上是相等的。

公式如下:
∣A−B∣< ϵ

其中 ∣A−B∣ ‌ 是 两个数的绝对差值ϵ ‌ 是你设定的 容忍误差阈值


Go 语言如何实现差值容限?


方法一:固定容限(适用于已知数据范围较小的场景)

定义一个常量 epsilon ,通常设为 1e-9 (对于 float64)或 1e-6 (对于 float32)。

go 复制代码
package main

import (
    "fmt"
    "math"
)


// 定义容限,根据业务精度需求调整
const epsilon = 1e-9

// IsEqual 判断两个 float64 是否 "近似相等"
func IsEqual(f1, f2 float64) bool {
    return math.Abs(f1-f2) < epsilon
}

func main() {
    f1 := 0.1 + 0.2
    f2 := 0.3

    if IsEqual(f1, f2) {
       fmt.Println("a 和 b 近似相等")
    } else {
       fmt.Println("a 和 b 不相等")
    }
}
bash 复制代码
a 和 b 近似相等

方法二:相对容限(适用于数据跨度大的场景,更严谨)

如果参与比较的数字 非常大 (如 1e20)或 非常小 (如 1e-20),固定的 1e-9 可能不再适用。

  • 对于 大数1e-9 太严格,正常的舍入误差可能超过它。
  • 对于 小数1e-9 太宽松,可能导致不相关的数被判为相等。

此时应使用‌ 相对误差 ‌,即 容限随数值的大小动态变化

go 复制代码
package main

import (
    "fmt"
    "math"
)

// IsApproximatelyEqual 使用相对容限比较
func IsApproximatelyEqual(f1, f2, epsilon float64) bool {
    // 处理特殊情况:如果两个数都非常接近0,直接比较绝对差
    if math.Abs(f1) < epsilon && math.Abs(f2) < epsilon {
       return math.Abs(f1-f2) < epsilon
    }

    // 相对误差公式: |f1 - f2| / max(|f1|, |f2|) < epsilon 等价于: |f1 - f2| < max(|f1|, |f2|) * epsilon
    // 分母取两者中较大的绝对值,避免除以零或过小值
    largest := math.Max(math.Abs(f1), math.Abs(f2))
    return math.Abs(f1-f2) <= largest*epsilon
}

func main() {
    // 场景:大数运算
    // 在 float64 中,1e15 的精度大约是 1e-1 (即小数点后 1 位左右是可靠的)
    // 任何更小的尾数变化都可能因为舍入而丢失或产生较大绝对误差

    // 让我们构造一个更典型的 "计算后应相等但存在误差" 的例子
    // 比如:(1e15 + 0.1) - 1e15 理论上等于 0.1
    // 但实际上,由于 1e15 太大,0.1 加上去可能被舍入掉,或者产生微小偏差

    f1 := 1e15
    f2 := 1e15 + 0.1
    f3 := f2 - 1e15

    fmt.Printf("原始值 f1: %.20f\n", f1)
    fmt.Printf("中间值 f2 (f1 + 0.1): %.20f\n", f2)
    fmt.Printf("计算值 f3 (f2 - f1): %.20f\n", f3)
    fmt.Printf("期望值: 0.1\n")
    fmt.Printf("绝对差值 |f3 - 0.1|: %.20e\n", math.Abs(f3-0.1))

    epsilon := 1e-9

    fmt.Println("\n--- 比较 f3 和 0.1 ---")

    relativeResult := IsApproximatelyEqual(f3, 0.1, epsilon)
    fmt.Printf("相对容限 (%.0e): %v\n", epsilon, relativeResult)
    if relativeResult {
       fmt.Println("-> 成功原因:虽然绝对差值大,但相对于数值大小,误差在允许范围内")
    }

    // 另一个更极端的例子:两个非常大的数,它们只差一点点,但绝对差值很大
    fmt.Println("\n--- 极端大数比较 ---")
    x := 1e20
    y := 1e20 + 1e10 // 相差 100亿,相对于 1e20 来说很小

    fmt.Printf("x: %.2e\n", x)
    fmt.Printf("y: %.2e\n", y)
    fmt.Printf("绝对差值: %.2e\n", math.Abs(x-y))

    // 相对容限:差值 1e10 / 最大值 1e20 = 1e-10,小于 1e-9,所以成功
    fmt.Printf("相对容限 (%.0e) 比较 x 和 y: %v (成功,因为相对误差仅为 1e-10)\n", epsilon, IsApproximatelyEqual(x, y, epsilon))
}
bash 复制代码
原始值 f1: 1000000000000000.00000000000000000000
中间值 f2 (f1 + 0.1): 1000000000000000.12500000000000000000
计算值 f3 (f2 - f1): 0.12500000000000000000
期望值: 0.1
绝对差值 |f3 - 0.1|: 2.49999999999999944489e-02

--- 比较 f3 和 0.1 ---
相对容限 (1e-09): false

--- 极端大数比较 ---
x: 1.00e+20
y: 1.00e+20
绝对差值: 1.00e+10
相对容限 (1e-09) 比较 x 和 y: true (成功,因为相对误差仅为 1e-10)

如何选择容限值?

数据类型 推荐默认容限 说明
float32 1e-6 精度约为 7 位十进制数字
float64 1e-9 ~ 1e-15 精度约为 15-17 位十进制数字; 一般业务用 1e-9 足够; 科学计算可能需要更小。

注意:

  • 金融/货币场景 ‌:‌不要 ‌使用浮点数比较,即使加了容限也不推荐。应将金额转换为整数(如 "分" )进行精确比较,或使用 github.com/shopspring/decimal 等高精度库。
  • 图形/物理引擎 ‌:通常使用固定容限(如 1e-5),因为坐标值范围通常可控。

总结

Go 语言中比较浮点数时:

  • 严禁 ‌ 直接使用 ==
  • 常用做法 ‌:计算两数之差的绝对值 math.Abs(a - b)
  • 判断标准 ‌:检查该差值是否小于你定义的‌ 差值容限(epsilon) ‌。
  • 进阶做法 ‌:若数值范围差异巨大,使用 相对容限 比较。

相关推荐
红尘散仙28 分钟前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
卷毛的技术笔记2 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
会编程的土豆2 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
喵个咪2 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
basketball6163 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
qq_2518364573 小时前
SpringBoot+Vue 共享电池柜管理系统 完整实现 前后端分离项目实战 完整代码
vue.js·spring boot·后端
zhangxingchao3 小时前
AI 大模型核心六:量化、Workflow 与 Agent、多轮 RAG
前端·人工智能·后端
IT_陈寒4 小时前
Vite打包时遇到的坑,原来问题出在这里
前端·人工智能·后端
ayqy贾杰5 小时前
基层管理的三板斧,在AI时代行不通了
前端·后端·团队管理
Apifox5 小时前
Apifox 5 月更新|Postman 导入优化、Runner 支持非 root 运行、请求代码自动带鉴权
前端·后端·安全