序言
🐒:
我在学习学习 Go 语言的 数据类型 中的 浮点型 时遇到的不解:浮点数为什么不能直接比较?要比较的话推荐用 差值容限 ?什么是 差值容限 ?我去找 deepseek 问了一下,整理出了这篇文章。
差值容限
🐋:
在 Go 语言(以及大多数遵循 IEEE 754 标准的编程语言)中,差值容限(Epsilon) 是指在进行浮点数比较时,允许两个数值之间存在的最大微小误差范围。
一、为什么需要差值容限?
🐋:
计算机使用 二进制 存储浮点数(float32或float64),许多十进制小数(如 0.1、0.2)在二进制中是 无限循环小数,无法被精确表示,只能存储为一个近似值,这导致运算结果会出现微小的精度丢失。
经典案例:
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)
}
0.30000000000000004441
false
直接使用
==比较会返回false,因为 sum 实际上比 0.3 大了一点点。为了解决这个问题,我们引入 差值容限。
二、差值容限的含义
🐋:
差值容限 是一个极小的正数(通常记为 ϵϵ 或 epsilon)。如果两个浮点数 AA 和 BB 的差的绝对值小于这个容限,我们就认为它们在 “工程意义” 或 “业务意义” 上是相等的。
公式如下:
∣A−B∣< ϵ
其中 ∣A−B∣ 是 两个数的绝对差值;ϵ 是你设定的 容忍误差阈值。
Go 语言如何实现差值容限?
方法一:固定容限(适用于已知数据范围较小的场景)
定义一个常量 epsilon,通常设为 1e-9(对于
float64)或 1e-6(对于float32)。
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 不相等")
}
}
a 和 b 近似相等
方法二:相对容限(适用于数据跨度大的场景,更严谨)
如果参与比较的数字 非常大(如
1e20)或 非常小(如1e-20),固定的1e-9可能不再适用。
- 对于 大数,
1e-9太严格,正常的舍入误差可能超过它。- 对于 小数,
1e-9太宽松,可能导致不相关的数被判为相等。
此时应使用 相对误差,即 容限随数值的大小动态变化:
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 := 1e20
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))
}
原始值 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+20): true
-> 成功原因:虽然绝对差值大,但相对于数值大小,误差在允许范围内
--- 极端大数比较 ---
x: 1.00e+20
y: 1.00e+20
绝对差值: 1.00e+10
相对容限 (1e+20) 比较 x 和 y: true (成功,因为相对误差仅为 1e-10)
方法三:混合模式(当你无法预知输入数据的量级范围时)
在 Go 语言中,为了兼顾 固定容限(Absolute Tolerance) 和 相对容限(Relative Tolerance) ,通常采用 “混合模式” 比较策略。这种策略的核心思想是:取两者中较大的那个容限值作为最终的判断标准。
为什么需要混合模式?
- 固定容限的短板:当数值非常大时(如
1e20),浮点数的舍入误差可能远超固定的1e-9,导致误判为不相等。- 相对容限的短板:当数值非常接近 0 时(如
1e-20),相对误差公式中的分母趋近于 0,会导致计算结果不稳定,或者将两个实际上差异很大的微小数值错误地判定为相等。混合模式公式:
∣a−b∣≤ max(absolute_tolerance,relative_tolerance×ma x (∣a∣,∣b∣) )
- 如果数值很小,主要依赖 固定容限(避免除以零或极小值带来的问题)。
- 如果数值很大,主要依赖 相对容限(允许误差随数值规模放大)。
package main
import (
"fmt"
"math"
)
// Float64Equal 使用混合容限比较两个 float64 是否近似相等
// absTol: 绝对容限,用于处理接近 0 的数值
// relTol: 相对容限,用于处理大数值
func Float64Equal(f1, f2, absTol, relTol float64) bool {
// 1. 处理特殊情况:NaN 和 Inf
if math.IsNaN(f1) || math.IsNaN(f2) {
return false // NaN 不等于任何值,包括它自己
}
if math.IsInf(f1, 0) || math.IsInf(f2, 0) {
return f1 == f2 // 只有同号无穷大才相等
}
// 2. 计算绝对差值
diff := math.Abs(f1 - f2)
// 3. 计算相对容限部分
// 取两者中较大的绝对值作为基准,避免除以极小值
largest := math.Max(math.Abs(f1), math.Abs(f2))
// 4. 混合判断:
// 最终允许的误差 = max(绝对容限, 相对容限 * 最大数值)
allowedError := math.Max(absTol, relTol*largest)
return diff <= allowedError
}
func main() {
// 定义常用的容限值
const absTol = 1e-9
const relTol = 1e-9
// 测试特殊值
fmt.Println("NaN vs NaN:", Float64Equal(math.NaN(), math.NaN(), absTol, relTol))
fmt.Println("+Inf vs +Inf:", Float64Equal(math.Inf(1), math.Inf(1), absTol, relTol))
fmt.Println("+Inf vs -Inf:", Float64Equal(math.Inf(1), math.Inf(-1), absTol, relTol))
// 测试常规场景
fmt.Println("1e-12 vs 2e-12:(绝对容限主导)", Float64Equal(1e-12, 2e-12, absTol, relTol))
fmt.Println("1e20 vs 1e20+1:(相对容限主导)", Float64Equal(1e20, 1e20+1, absTol, relTol))
}
NaN vs NaN: false
+Inf vs +Inf: true
+Inf vs -Inf: false
1e-12 vs 2e-12:(绝对容限主导) true
1e20 vs 1e20+1:(相对容限主导) true
方法四:使用第三方库
如果你需要比较包含浮点数的 结构体(
Struct)或 切片(Slice),go-cmp是最佳选择。它允许你定义全局的比较选项。
安装相关依赖
go get github.com/google/go-cmp/cmp
go get github.com/google/go-cmp/cmp/cmpopts
package main
import (
"fmt"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
type Config struct {
Name string
Score float64
Version float32
}
func main() {
c1 := Config{Name: "A", Score: 0.1 + 0.2, Version: 1.0}
c2 := Config{Name: "A", Score: 0.3, Version: 1.0}
// 使用 cmpopts.EquateApprox 进行混合模式比较
// 参数1: fraction (相对容限, 即 relTol)
// 参数2: threshold (绝对容限, 即 absTol)
// 含义:允许相对误差为 1e-9,或者绝对误差小于 1e-9
opts := cmpopts.EquateApprox(1e-9, 1e-9)
if cmp.Equal(c1, c2, opts) {
fmt.Println("✅ 结构体中的浮点数近似相等")
} else {
fmt.Println("❌ 不相等")
// 打印详细差异
fmt.Println(cmp.Diff(c1, c2, opts))
}
c3 := Config{Name: "B", Score: 1e-9, Version: 1.0}
c4 := Config{Name: "B", Score: 1, Version: 1.0}
opts = cmpopts.EquateApprox(1e-9, 1e-9)
if cmp.Equal(c3, c4, opts) {
fmt.Println("✅ 结构体中的浮点数近似相等")
} else {
fmt.Println("❌ 不相等")
// 打印详细差异
fmt.Println(cmp.Diff(c3, c4, opts))
}
}
✅ 结构体中的浮点数近似相等
❌ 不相等
main.Config{
Name: "B",
- Score: 1e-09,
+ Score: 1,
Version: 1,
}
优势:
- 递归比较:自动深入结构体内部,对所有
float32/64字段应用容限规则。- 混合模式:
EquateApprox(fraction, threshold)本质上就是实现了我们之前讨论的 混合模式。
如何选择容限值?
| 数据类型 | 推荐默认容限 | 说明 |
|---|---|---|
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) 。
- 进阶做法:若数值范围差异巨大,使用 相对容限 比较。