不能用 NewFromFloat 初始化 Decimal,因为 float64 的 0.1 本身是二进制近似值(如 0.10000000000000000555...),NewFromFloat 仅忠实地转存该误差,导致后续所有运算基于偏差起点;正确做法是用 NewFromString("0.1") 或 NewFromInt(123) 精确初始化。

为什么不能用 NewFromFloat 初始化 Decimal?
直接传 float64 给 decimal.NewFromFloat(0.1),看似方便,实则埋雷。因为 0.1 在内存里本就是近似值(0.10000000000000000555...),NewFromFloat 只是忠实地把这个误差“转存”成 Decimal,后续所有运算都在这个带偏差的起点上展开。
正确做法是:从字符串或整数初始化。
-
decimal.NewFromString("0.1")—— 精确解析十进制字面量 -
decimal.NewFromInt(123)或decimal.New(123, 0)—— 整数无损 - 若必须从已有
float64转换,先用fmt.Sprintf格式化到所需位数再解析,例如:s := fmt.Sprintf("%.2f", f); d, _ := decimal.NewFromString(s)
Round 和 Truncate 的行为差异
两者都接受一个 precision 参数(小数点后保留几位),但逻辑完全不同:
-
d.Round(2)是四舍五入:decimal.NewFromString("1.235").Round(2)→"1.24" -
d.Truncate(2)是直接截断:decimal.NewFromString("1.235").Truncate(2)→"1.23"
金融场景中,“四舍五入”是默认规则,但某些对账或审计要求“向下取整”,就得用 Truncate;误用会导致每笔交易差 0.01 元,积少成多就是事故。
立即学习“go语言免费学习笔记(深入)”;
除法必须指定 divPrecision
Decimal.Div() 不像加减乘那样“自动决定精度”。它强制要求你传第二个参数——除法结果最多保留多少位小数,否则 panic。
例如:a.Div(b, 2) 表示结果保留 2 位小数(按四舍五入);a.Div(b, 4) 就是 4 位。
常见错误是漏写这个参数,或写成 a.Div(b, 0) 导致结果被截断为整数,比如 10.Div(3, 0) 得 3,而非预期的 3.33。
GORM 中字段定义和 JSON 序列化要注意什么?
在结构体中声明金额字段时,必须用 decimal.Decimal 类型,而不是 float64 加 sql:"type:decimal" 标签:
type Order struct {
ID uint `gorm:"primaryKey"`
Amount decimal.Decimal `json:"amount"`
}
原因很实在:GORM 的 Valuer/Scanner 接口实现只对 decimal.Decimal 生效,能确保数据库 DECIMAL ↔ Go 值全程不经过 float64 中转。一旦混用 float64,哪怕数据库存得再准,Go 层读出来就已失真。
另外,decimal.Decimal 默认 JSON 输出是字符串(如 "123.45"),避免前端 JS 解析 float64 时二次失真,这点不用额外配置,但得心里有数——别试图用 json.Number 去强转。
最易被忽略的是:所有参与计算的中间变量,只要涉及金额或精度敏感值,都得是 decimal.Decimal 类型。哪怕只是临时做一次 if d.GreaterThan(decimal.NewFromInt(0)),也不能用 float64 比较。


















