必须避免用 float64 处理货币,因其精度丢失不可逆;应统一使用 decimal.decimal 或 int64(单位“分”),绑定时确保前端传字符串或拦截非字符串数字,全程杜绝 float64 参与计算。

别用 float64 做货币计算,Gin 本身不干预数值类型,但一旦你把金额从 JSON 或表单解析成 float64,精度就已不可逆地丢失了。
为什么 Gin 的 ShouldBindJSON 默认不帮你防浮点陷阱
Gin 的绑定逻辑只做类型映射,不校验语义。当你定义一个结构体字段为 float64 并用 c.ShouldBindJSON(&v) 解析 {"amount": 19.99},Go 会先用标准 JSON 解码器把 19.99 转成 float64——这个过程已经失真(19.99 在内存中实际是 19.989999999999998)。
- 错误示范:
type Order struct { Amount float64 `json:"amount" binding:"required"` } - 正确做法:字段必须用高精度类型,如
decimal.Decimal或int64(单位“分”) - 如果前端传的是字符串
"19.99",可先绑定为string,再用decimal.NewFromString()安全转换 - 若必须兼容旧协议传数字,应在中间件里拦截并拒绝
float64类型的金额字段(检查 JSON token 类型),或强制转字符串再解析
如何让 Gin 绑定 decimal.Decimal 字段
Gin 依赖 encoding/json 的反射机制,而 shopspring/decimal.Decimal 已实现 UnmarshalJSON 方法,因此可直接绑定,但需注意初始化方式。
- 结构体定义要显式指定 JSON 标签:
Amount decimal.Decimal `json:"amount" binding:"required"` - 禁止用
decimal.NewFromFloat64(19.99)初始化,否则刚进 Go 就失真;改用decimal.NewFromString("19.99")或decimal.New(1999, 2) - 绑定失败时,
ShouldBindJSON会返回json: cannot unmarshal number into Go struct field ... of type decimal.Decimal—— 这说明前端传了非字符串数字,需前端修正或加中间件兜底 - 若想统一处理所有金额字段,可自定义
binding.StructValidator,在验证前对decimal.Decimal字段做字符串预转换
金额格式化输出时如何避免二次失真
Gin 的 c.JSON() 会调用 json.Marshal,而 decimal.Decimal 默认序列化为字符串(如 "19.99"),这是安全的;但若你手动用 fmt.Sprintf("%.2f", d.InexactFloat64()),就又掉回 float64 陷阱。
- 推荐直接返回
decimal.Decimal,让它自己MarshalJSON输出字符串 - 需要带千分位和货币符号时,先转整数单位(如
d.Mul(decimal.NewFromInt(100)).IntPart()得到“分”),再用golang.org/x/text/message格式化 - 切忌在格式化前调用
d.InexactFloat64()或d.Float64(),这两个方法会主动引入浮点误差 - 如果 API 要求固定两位小数字符串(如
"12345.67"),用d.String()即可;它始终精确,且自动补零
整数法与 decimal 的边界怎么划
整数法(单位“分”)最稳,decimal 更灵活,但两者混用极易出错。
- 数据库字段类型:整数法用
BIGINT,decimal法用DECIMAL(20,8),GORM 中通过sql:"type:decimal(20,8)"显式声明 - 内部计算:整数法全程
int64,decimal法全程decimal.Decimal,禁止出现float64中间变量 - 拆分、均分等需分数结果的场景(如 1 分钱分 3 人),必须切换到
math/big.Rat,decimal无法表示 1/3 的精确值 - Gin 层只负责透传和校验,真正的精度控制发生在 service 层——这里才是决定用哪种类型的关键决策点
真正难的不是选哪个库,而是让整个数据流(HTTP → 绑定 → 计算 → 存库 → 查询 → 渲染)全程不触碰 float64。任何一环松动,前面所有努力都会归零。











