float64不能用于财务计算,因其二进制表示无法精确存储十进制小数(如0.1是无限循环二进制),初始截断即引入误差,后续所有运算均在此失真基础上进行,导致“一分钱”偏差不可避免。

float64 不能用于财务计算,哪怕只做一次加减或四舍五入,都可能产生“一分钱”偏差。必须用十进制固定点类型替代。
为什么 float64 在金额场景下必然出错
不是精度不够,而是表示方式根本错位:0.1 在二进制中是无限循环小数(0.0001100110011...),float64 强行截断后存储的是近似值。后续所有运算都在这个带误差的底子上进行。
常见错误现象包括:
-
0.1 + 0.2 == 0.3返回false -
math.Round(2.555 * 100) / 100得到2.55而非2.56 - 多笔订单累加后与数据库总和对不上,差额稳定为几厘几毫
这些不是偶发 bug,是 IEEE 754 标准下的确定性行为。只要用 float64 存金额、算汇率、做分账,就一定会在某个时间点暴露。
立即学习“go语言免费学习笔记(深入)”;
用 github.com/shopspring/decimal 替代浮点数
这是 Go 生态中事实标准的十进制高精度库,不依赖 math/big 的泛型开销,底层用整数+缩放因子实现,性能足够服务端高频记账。
关键使用要点:
- 所有金额字段声明为
decimal.Decimal,而非float64或int64(后者需自行维护单位,易错) - 初始化必须用
decimal.NewFromInt或decimal.NewFromString,禁用decimal.NewFromFloat(它会把浮点误差带进来) - 四舍五入统一走
.RoundBank(<code>scale),避免用math.Round再转 decimal - 数据库交互时,GORM 支持
decimal.Decimal直接映射到DECIMAL字段,无需手动转字符串
示例:
price := decimal.NewFromInt(1999) // 表示 19.99 元(单位:分)
taxRate := decimal.NewFromString("0.06") // 6% 税率
total := price.Mul(taxRate.Add(decimal.NewFromInt(1))).RoundBank(2)
// total.String() == "21.19"
数据库字段与迁移注意事项
PostgreSQL/MySQL 的 DECIMAL(p,s) 是唯一安全选择。若用 BIGINT 存“分”,看似简单,但会在以下环节埋雷:
- 前端传参时,用户输入
"19.99"需手动乘 100,JSON 解析易错(如科学计数法变成1.999e1) - 跨服务调用时,不同语言对“分”单位的处理不一致(Java 的
longvs Go 的int64溢出边界不同) - 聚合查询如
SUM()、AVG()在数据库层仍需除以 100,中间结果若被截断不可逆
推荐建表语句:
amount DECIMAL(18,2) NOT NULL COMMENT '订单金额,单位:元'
注意 18,2 覆盖了绝大多数业务(最大支持 999999999999999.99 元),且兼容 MySQL 5.7+ 与 PostgreSQL。
容易被忽略的隐式转换陷阱
最危险的不是不会用库,而是“以为用了,其实没用”。以下写法都会悄悄退化回 float64:
- 从 JSON 直接解码到
struct { Amount float64 },再赋值给decimal.Decimal - 用
fmt.Sprintf("%.2f", d.InexactFloat64())做展示(InexactFloat64()就是触发失真的源头) - 在 GORM 的
Where条件里写Where("amount > ?", 99.99)—— 这里的99.99是float64字面量
正确做法是:所有外部输入(JSON、URL 参数、SQL 条件值)必须先经 decimal.NewFromString 或 decimal.NewFromInt 构造,再参与运算或查询。
真正难的从来不是选哪个库,而是让整个调用链——从 HTTP 请求头、到 DB 查询条件、再到日志打印——全都守住十进制边界。漏掉任意一环,前面的努力就白做了。


















