金额必须用 string 或 int64 初始化 decimal,禁用 newfromfloat;gorm 映射需字段类型为 decimal.decimal 且配 type:decimal(20,8);运算后立即 round(2);json 默认序列化为字符串防 js 精度丢失。

别用 NewFromFloat 构造金额,这是最常踩的精度坑
前端传“19.99”、数据库存 DECIMAL(20,8),结果 Go 层一写 decimal.NewFromFloat(19.99),底层就已是 19.989999999999998——这不是 decimal 库的问题,是 float64 本身无法精确表示十进制小数的硬伤。
- 所有金额、费率、比例等业务值,必须从
string或int64 + scale初始化:用decimal.NewFromString("19.99"),或decimal.New(1999, 2)(表示 1999/10²) -
NewFromFloat仅适合调试或非关键场景;生产环境用它等于主动放弃精度控制权 - JSON 反序列化时,
decimal.Decimal默认支持字符串输入(如"amount": "19.99"),但若前端误传数字"amount": 19.99,Go 会先走 float64 解析再转 decimal,照样丢精度
GORM 映射 DECIMAL 字段必须配对使用 decimal.Decimal 和 gorm:"type:decimal(20,8)"
只在 struct tag 里写 sql:"type:decimal(20,8)" 不够,如果字段类型还是 float64,GORM 扫描时仍会用二进制浮点解析数据库的十进制值,误差当场产生。
- GORM v2 推荐写法:
Amount decimal.Decimal `gorm:"type:decimal(20,8);not null"` - 确保
decimal.Decimal实现了driver.Valuer和sql.Scanner,能自动完成[]byte ↔ decimal.Decimal转换 - 用 pgx 驱动时,更推荐显式注册
pgtype.Numeric类型,比依赖 interface{} 更安全可控 - 迁移时加
AutoMigrate会按 tag 创建正确列类型;但已有表需手动确认数据库字段确实是DECIMAL,而非FLOAT或DOUBLE
运算后该舍入时立刻 Round,别等最后一步
decimal 的运算是精确的,但业务上“保留两位小数”不是显示需求,而是语义要求——比如手续费必须按银行家舍入到分,多出的 0.001 元不参与后续累加。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 乘除后立即调用
.Round(2),而不是留到fmt.Printf或 JSON 输出前才处理 - 避免链式调用中漏掉舍入:
price.Mul(qty).Mul(discount)得到的是高精度中间值,应写成price.Mul(qty).Mul(discount).Round(2) -
Round默认用银行家舍入(四舍六入五成双),符合金融规范;不用Truncate或手动floor * 100 / 100,那会引入偏差
JSON 序列化默认输出字符串,后端要数字就得自己接管 MarshalJSON
前端框架(如 Axios)默认把 JSON 数字当 float 解析,如果后端返回 "amount": "19.99"(string),前端可能报类型错误;但若强行让 decimal 输出数字,又得绕过它的安全设计。
- 默认行为合理:decimal 的
MarshalJSON返回字符串,防止 JS 浮点失真(0.1 + 0.2 !== 0.3) - 若下游明确要求 number 类型,需自定义 marshaler:
func (d Decimal) MarshalJSON() ([]byte, error)里调用d.String()再json.Marshal成 float64 ——但注意,这步会重新引入浮点误差,仅限内部可信系统 - 更稳妥的做法是统一约定前端接收字符串金额,并做校验(正则
^\d+(\.\d{1,2})?$),彻底规避 JS 精度问题
真正难的不是调用哪个函数,而是所有输入入口——HTTP body、DB scan、配置文件、RPC 参数——都得守住“字符串/整数进,decimal 出”这一条线。漏掉任意一个环节,前面做的精度保障就归零。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










