float32 与 float64 精度陷阱:如何在 Go 中避免错误使用

让我们看看这段简单的代码:

复制代码
 var n float64
 for i := 0; i < 10; i++ {
  n += 0.1
 }
 fmt.Println(n)
 fmt.Println(n == 1)

你可能想不到,输出结果竟然是......

复制代码
0.9999999999999999
false

那么,换成 float32 又是怎样呢?

复制代码
 var n float32
 for i := 0; i < 10; i++ {
  n += 0.1
 }
 fmt.Println(n)
 fmt.Println(n == 1)

现在你可能有点困惑了,输出将会是......

复制代码
1.0000001
false

到底发生了什么?

用一句话解释:这其实是浮点数在 CPU 中的表示方式导致的副作用。

想了解更多信息,你可以阅读这篇文章(https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html)。

所以不要使用 float 来计算任何与金钱相关的逻辑。

对于应用中的这类问题,我发现这个 Go 库是一个很实用的简洁解决方案:

shopspring/decimal(github.com/shopspring/decimal): Go 中的任意精度定点小数。

重构之后的代码:

复制代码
import "github.com/shopspring/decimal"

func main() {
 m := decimal.NewFromFloat(0)
 for i := 0; i < 100; i++ {
  m = m.Add(decimal.NewFromFloat(0.01))
 }
 fmt.Println(m.InexactFloat64())
}

现在,输出结果将会是......

复制代码
1

核心原因:二进制浮点数无法精确表达十进制小数

CPU 内部全部用二进制存储浮点数(IEEE‑754 标准) 。 十进制的0.1,转换成二进制是无限循环小数

复制代码
0.1(10) = 0.0001100110011...(2) 无限循环

float32/float64存储空间有限,不能保存无限位数,只能截断、保存一个近似值。

  • 每次+=0.1,实际加的不是严格等于 0.1,是一个近似值;
  • 循环累加 10 次后,误差不断累积;
  • float64 误差偏小,结果略小于 1 → 0.9999999999999999
  • float32 精度更低,截断误差更大,结果略大于 1 → 1.0000001

所以直接用==比较浮点数是大忌,不要直接判断两个浮点数相等。

著名论文:What Every Computer Scientist Should Know About Floating‑Point Arithmetic,就是文中给的 Oracle 链接,讲 IEEE754 浮点数坑。

为什么金钱计算不能用 float?

金额会有小数(0.01 元),0.01同样是二进制无限循环小数。 多次加减乘除,误差持续累积:1 分钱误差,循环上万次就变成几元、几十元,对账完全错乱。

二个常规解决思路

  1. 整数方案:全部用分做单位,比如 1.23 元直接存 123,全部 int64 运算,输出的时候再除以 100。简单高效。
  2. 定点高精度库 ,Go 生态常用shopspring/decimal,内部用大整数实现十进制定点运算,绕开二进制浮点数。

重点总结

  1. float32/float64(IEEE‑754):二进制存储,不能精确表示 0.1、0.02、0.03 这类十进制小数
  2. 多次运算误差累积,不能用==做相等判断;
  3. 金额计算:要么放大为整数,要么使用 decimal 定点库;
  4. decimal 库尽量从字符串初始化,不要从 float64 转,否则会把原始浮点数误差带入进去。

关于4的解释:

发生了什么?

9007199254740993.0 这个数字已经超出 float64 的整数精确范围(2⁵³) 。 float64 根本存不下这个整数,存入的时候直接就丢失 1 ,强制变成 9007199254740992

注意:失真发生在 f := 9007199254740993.0 这一步,还没进到 decimal 库 。 decimal 的 NewFromFloat 拿到的已经是被篡改过的值,无论库内部怎么做最短往返转换,也不可能把丢掉的那 1 给恢复回来。

NewFromString("9007199254740993.0"),不走 float64,直接解析文本,完整保存原始数字,结果完全正确。

业务上对应的真实场景

JSON 接口,如果前端把大金额数字用 number 类型传给后端,而不是字符串。

前端 JS 数字就是 IEEE754 float64,超过 2⁵³ 的数字直接失真。

后端 Go 把这个数字解析成 float64,再调用NewFromFloat,金额直接错。

✅正确方案:前端传字符串 "9007199254740993.0",后端直接NewFromString

提炼面试关键点(非常高频)

  1. shopspring/decimalNewFromFloat不是库本身有 bug,问题根源是传入的 float64 已经发生信息丢失;新版本只是对部分小数做 "尽力修复",不能兜底全部情况。
  2. float64 不光小数会出问题,大整数同样会丢失精度,超过 2^53 就无法精确表达全部整数
  3. 金额业务铁律:源头尽量使用字符串输入,避开 float64
  4. 禁止:decimal.NewFromFloat(xxxFloat); 推荐:NewFromString("123.45")
相关推荐
Rain的Java大神之路1 小时前
介绍一下分布式事务
java·分布式·后端·spring·spring cloud·架构·springcloud
指尖时光.1 小时前
Three.js 超全入门综合案例
开发语言·javascript·ecmascript
暴力求解2 小时前
Linux网络---传输层协议TCP(二)
linux·服务器·开发语言·网络·tcp/ip
吴声子夜歌2 小时前
Java面试题——基础(一)
java·开发语言
小庞在加油2 小时前
WinDbg实战:QT/跨平台项目死锁与无响应问题排查指南
开发语言·qt·windbg·工具
Vae_Mars3 小时前
C#中的delegate委托
开发语言·c#
一直走下去-明3 小时前
简单的http抓包解包完整代码
开发语言·python
caimouse3 小时前
ReactOS 图形系统分析(51):元文件子系统 — metafile.c
c语言·开发语言
学习星球4 小时前
Solid.js 实战:拆解官方 RealWorld 项目
开发语言·javascript·vue.js