float64无法精确表示0.1等十进制小数是IEEE 754双精度浮点数固有局限,因0.1在二进制中为无限循环小数需截断,导致后续所有运算基于失真值,引发==判断失败、math.Floor偏差、金融计算误差等问题。

float64 无法精确表示 0.1、0.000000001 这类十进制小数
这不是 Go 的 bug,而是所有 IEEE 754 双精度浮点数的共性限制:float64 用 52 位二进制尾数表示数值,而像 0.1 在二进制中是无限循环小数(0.0001100110011...),必须截断存储。一旦赋值,误差就已产生——后续所有运算都在这个失真值上进行。
常见错误现象:
-
0.1 + 0.2 == 0.3返回false -
math.Floor(2.4 / 0.8)得到2而非3(因为运行时计算结果实为2.9999999999999996) - 金融计算中
28860.000000001 - 28800输出60.000000001000444,而非预期的60.000000001
验证方法:用 fmt.Printf("%.20f", x) 查看真实存储值,能暴露隐藏的尾数偏差。
用 fmt.Sprintf + strconv.ParseFloat 截断精度反而放大误差
很多人想“四舍五入到小数点后两位”,就写 strconv.ParseFloat(fmt.Sprintf("%.2f", x), 64)。这看似合理,实则危险:
立即学习“go语言免费学习笔记(深入)”;
- 字符串格式化本身会做就近舍入,但
fmt.Sprintf的舍入规则和底层二进制表示不一致 - 再用
strconv.ParseFloat解析时,又经历一次二进制近似,相当于误差叠加 - 对大数(如
1e15)+ 小数(如0.1)做%.2f,可能直接丢失小数部分(因 float64 有效位仅约 15–17 位)
正确做法:若仅用于显示,直接 fmt.Printf("%.2f", x);若需参与后续计算,不要转回 float64,改用高精度类型。
math.Floor / math.Ceil / == 判断在浮点场景下极易出错
这些函数和操作符对输入极其敏感,微小的表示误差就会导致逻辑跳变:
-
math.Floor(x)对2.9999999999999996返回2,而人眼认为它“就是 3” -
a == b几乎永远不该用于float64,哪怕两个值看起来一样 -
int(x)截断时,若x实际是2.9999999999999996,结果是2,不是3
替代方案:
- 相等判断:改用
math.Abs(a-b) (根据业务容忍度调 <code>tolerance) - 取整需求:先加容差再取整,例如
math.Floor(x + 1e-12),或直接用decimal库的.RoundFloor() - 整数转换:优先用
decimal的.IntPart()或.Round(0).IntPart()
高精度场景必须绕过 float64 中间表示
用 decimal.NewFromFloat(28860.000000001) 是错的——28860.000000001 字面量在传入前已被转成失真的 float64,误差已固化。
真正安全的做法只有两种:
- 原始输入是字符串:
decimal.RequireFromString("28860.000000001") - 原始输入可换算为整数比例(如金额用“分”、时间用“纳秒”):
decimal.NewFromInt(28860000000001).Div(decimal.NewFromInt(1000000000))
别碰 math/big.Rat 做小数——它的 SetFloat64 方法同样从 float64 开始,一开头就输了。
最常被忽略的一点:精度问题从来不是“算得不准”,而是“存得不准”。所有修复动作必须从数据源头切断 float64 表示路径,否则越补越偏。


















