必须放弃float64:涉及人眼可感知十进制小数的等值判断、四舍五入或累加对账时,因IEEE 754二进制表示缺陷导致确定性失真;金融、支付、会计模块须改用decimal或int64归一化处理。

用 float64 做金额、汇率、计费类计算,迟早出一分钱对不上的问题;但盲目上 github.com/shopspring/decimal,又可能在高频场景下吃掉 3–5 倍 CPU 时间。精度和性能不是二选一,而是要按场景切分、按路径控制。
什么时候必须放弃 float64?
只要涉及“人眼可感知的十进制小数”且需等值判断、四舍五入或累加对账,float64 就不可信:
-
0.1 + 0.2 == 0.3返回false—— 这不是 Go 的 bug,是 IEEE 754 的宿命 -
math.Round(2.555 * 100) / 100得到2.55而非2.56—— 因为2.555 * 100在二进制中本就是255.49999999999997 - 订单金额累计 10 万笔后,与数据库 sum 字段差 0.01 —— 浮点误差在多次运算中被放大、不可逆
这些不是“小概率事件”,而是确定性失真。金融、支付、会计模块必须绕开 float64。
decimal.Decimal 性能代价在哪?
github.com/shopspring/decimal 本质是把小数转成整数(如 "123.45" → 12345,精度 2),所有运算都在整数层面做,再按 scale 拆回小数。代价主要来自:
立即学习“go语言免费学习笔记(深入)”;
- 每次构造
decimal.Decimal都涉及字符串解析或大整数拷贝,比float64赋值慢 10–20 倍 -
Decimal.Add、Decimal.Mul等方法内部有边界检查、scale 对齐、进位处理,无法内联,函数调用开销明显 - 高并发下,若频繁新建
Decimal实例(尤其在循环中),会显著抬高 GC 压力
实测:在 100 万次简单加法中,float64 耗时约 12ms,decimal.Decimal 约 68ms —— 差距接近 6 倍。这不是库写得差,是设计取舍。
如何在关键路径上保精度又控开销?
核心思路:**只在输入/输出边界做 decimal 转换,中间计算尽量用 int64(单位归一)**。
- 金额统一存为“分”(
int64),所有加减乘除、条件判断、DB 查询都走整数,零误差、零分配、零 GC - 仅在渲染前端、生成账单、对接第三方 API 时,才用
decimal.NewFromInt(amountInCents).Div(decimal.NewFromInt(100))转一次 - 避免在 for 循环里反复调用
decimal.NewFromString或Decimal.String()—— 这些是性能黑洞 - 如果必须用
decimal做中间计算(比如要支持动态小数位配置),至少复用decimal.Context,显式设置Precision和Rounder,避免每次都走默认全局上下文
例如计费引擎中,用户余额、扣费额度、优惠券面额全用 int64(单位:厘),只有最终返回给前端 JSON 时,才统一格式化为两位小数字符串 —— 这样既杜绝了精度泄漏,又把 decimal 使用压缩到最小粒度。
为什么不要自己实现“定点数”?
有人想用 type Money int64 + 手动管理小数位,看似轻量,但极易踩坑:
- 乘法后 scale 不匹配:
123.45 * 0.88→12345 * 88 = 1086360,结果该除以10000还是100?错一位就差 100 倍 - 除法精度丢失:整数除法直接截断,
100 / 3得33,而业务可能要求四舍五入或保留 2 位 - 序列化/反序列化时容易漏掉单位说明,JSON 里传
12345,前端不知道这是“元”还是“分”
decimal 库已封装好 scale 传播、舍入策略、溢出检测和 JSON 编解码行为,自己造轮子反而增加维护成本和出错概率。真正需要权衡的,从来不是“用不用 decimal”,而是“在哪一层、以什么粒度用”。



















