float64加减会丢精度是因为ieee 754用二进制近似表示十进制小数,导致0.1+0.2≠0.3;业务中应优先用int64(如分、纳秒),精度敏感字段才用decimal.decimal,且须避免decimal.newfromfloat,改用newfromstring或new构造。

为什么 float64 算加减会丢精度,而业务又不能总用 decimal.Decimal
因为 IEEE 754 浮点数本质是二进制近似表示十进制小数,0.1 + 0.2 != 0.3 不是 bug,是必然结果。金融、计费、库存扣减这类场景只要出现 0.29999999999999998 就可能触发资损或对账失败。
但直接把所有数字都换成 github.com/shopspring/decimal.Decimal 也不行:它用整数+缩放因子模拟定点数,运算慢、内存占用高、GC 压力大。实测在高频订单计算中,纯 decimal 比 float64 慢 3–5 倍,对象分配量翻 4 倍以上。
- 能用整数 cents/millis/units 表达的,优先转成
int64运算(例如金额存为分,时间存为纳秒) - 必须带小数且精度敏感的字段(如汇率、税率、重量),才用
decimal.Decimal - 临时中间计算(比如归一化、比例缩放)仍可用
float64,但最终落库/返回前必须 round 到业务要求的小数位并转成decimal或整数
decimal.NewFromFloat 是最危险的构造函数
它把一个已经失真的 float64 值强行塞进 decimal,等于把误差“固化”进高精度容器里。比如 decimal.NewFromFloat(0.1) 实际得到的是 0.1000000000000000055511151231257827021181583404541015625 —— 这比直接用 float64 还误导人。
- 从字符串构造:
decimal.NewFromString("0.1"),安全且明确 - 从整数+小数位构造:
decimal.New(1, -1)表示 1 × 10⁻¹ = 0.1 - 如果上游只能给
float64(比如 JSON 解析后),先用fmt.Sprintf("%.2f", f)格式化成字符串再转,别图省事调NewFromFloat
性能瓶颈常卡在 String() 和 Round()
decimal.Decimal.String() 内部要做格式化、缩放、符号处理,比 strconv.FormatFloat 慢一个数量级;Round() 涉及缩放因子重算和内部整数截断,高频调用时 CPU 火焰图里很扎眼。
立即学习“go语言免费学习笔记(深入)”;
- 日志打点、调试输出时,避免在循环里反复调
d.String(),改用fmt.Printf("%s", d)(它走自定义fmt.Stringer,有缓存优化) - 批量 Round 场景(如统一保留 2 位小数),用
d.Round(2)而不是d.Div(decimal.New(1, 0)).Round(2)—— 后者多一次除法和缩放 - 如果只是做等值比较(如
d.Equals(other)),完全不需要Round或String,直接比内部coeff和exp
JSON 序列化时 decimal 默认输出字符串,但前端不认
标准 json.Marshal 对 decimal.Decimal 默认输出字符串(如 "123.45"),而多数前端框架(Axios、fetch)期望数字类型。硬改成数字会丢失精度,保持字符串又得全局适配解析逻辑。
- 项目初期就统一约定:后端返回金额类字段全部用
string类型,前端显式parseFloat或保留字符串做展示(防 JS 浮点问题) - 若必须返回数字,自定义
MarshalJSON方法,用d.InexactFloat64()并加注释说明“仅用于展示,不可用于计算” - 别依赖
json.Number或第三方 tag(如json:",string")去绕过 —— 它们只影响字段类型,不解决底层精度问题
真正难的不是选 float64 还是 decimal,而是同一笔交易里哪些环节可以松、哪些必须死守精度边界。边界模糊的地方,往往就是线上对账差异的起点。











