gorm用float64存金额必然出错,因其二进制浮点表示无法精确表达十进制小数(如0.1、99.99),导致入库/读取不一致及误差累积;正确做法是全程使用shopspring/decimal.decimal,字段声明为decimal.decimal类型,数据库映射为decimal(20,2),初始化必须用newfromstring或new,禁用newfromfloat,运算调用add/sub等方法,json序列化需加,string tag。

为什么 GORM 用 float64 存金额一定会出错
因为 float64 是二进制浮点数,无法精确表示大多数十进制小数(如 0.1、99.99),导致入库值和读出值不一致,加减后误差累积。常见现象包括:99.99 存进数据库再查出来变成 99.98999999999999,或 math.Round(12.345*100)/100 得到 12.34 而非 12.35。
这不是 GORM 的 bug,而是所有基于 float64 的链路(输入 → 内存计算 → 序列化 → 数据库)共同失效的结果。只要中间出现一次 float64 中转,精度就不可逆地丢了。
shopspring/decimal 如何与 GORM 正确集成
GORM 本身不内置 decimal 类型,但 shopspring/decimal.Decimal 天然实现了 driver.Valuer 和 sql.Scanner 接口,可直接用于字段映射。
- 结构体字段必须声明为
decimal.Decimal类型,不能是float64或string - 数据库字段需显式指定精度,例如
gorm:"type:decimal(20,2);not null",避免依赖 GORM 自动推断 - 若用 GORM v2,无需额外注册类型;v1 需手动调用
sql.Register(已基本淘汰) - 迁移建表时,GORM 不会自动创建
DECIMAL字段,必须靠type:标签强制指定
初始化 decimal.Decimal 的安全写法
decimal.NewFromFloat 是高危函数——它先让 float64 表示该数,再转成 Decimal,此时精度已丢失。例如 decimal.NewFromFloat(2.01) 实际可能生成 2.0099999999999998。
正确初始化方式只应有以下两种:
- 从字符串:
decimal.NewFromString("2.01")(返回(Decimal, error),务必检查error) - 从整数+精度:
decimal.New(201, 2)(表示201 / 10² = 2.01)
前端传金额必须约定为带两位小数的字符串(如 "199.99"),后端不做任何 float64 解析,直通 NewFromString。
运算与序列化必须全程走 decimal 方法
所有加减乘除、比较、舍入都必须调用 decimal 提供的方法,禁止混用原生运算符或 math 包函数。
- 加法:
price.Add(tax),不是price + tax - 舍入:优先用
.RoundBank(2)(四舍六入五成双),比.Round(2)更符合金融规范 - JSON 输出:加
json:"amount,string"tag,防止前端解析成number类型再次失真 - 数据库映射:DECIMAL 字段建议映射为
int64(单位“分”)并配json:",string",这是更轻量且无歧义的方案
最容易被忽略的是 JSON 序列化环节:哪怕内存中全是 decimal.Decimal,一旦用了 json.Marshal 默认行为,就会触发 float64 中转——必须确保字段有 ,string tag 或自定义 MarshalJSON。











