让我们看看这段简单的代码:
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.23 元直接存 123,全部 int64 运算,输出的时候再除以 100。简单高效。
- 定点高精度库 ,Go 生态常用
shopspring/decimal,内部用大整数实现十进制定点运算,绕开二进制浮点数。
重点总结
float32/float64(IEEE‑754):二进制存储,不能精确表示 0.1、0.02、0.03 这类十进制小数;- 多次运算误差累积,不能用
==做相等判断; - 金额计算:要么放大为整数,要么使用 decimal 定点库;
- 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。
提炼面试关键点(非常高频)
shopspring/decimal的NewFromFloat,不是库本身有 bug,问题根源是传入的 float64 已经发生信息丢失;新版本只是对部分小数做 "尽力修复",不能兜底全部情况。- float64 不光小数会出问题,大整数同样会丢失精度,超过 2^53 就无法精确表达全部整数。
- 金额业务铁律:源头尽量使用字符串输入,避开 float64。
- 禁止:
decimal.NewFromFloat(xxxFloat); 推荐:NewFromString("123.45")。
