数学工具

shopspring/decimal 提供任意精度的十进制定点数,适合金额、税率等不能接受二进制浮点误差的场景。

go get github.com/shopspring/decimal

创建与运算

从外部读取小数时优先使用字符串,避免先转换为 float64 引入误差。

price, err := decimal.NewFromString("19.99")
if err != nil {
    return err
}

quantity := decimal.NewFromInt(3)
rate := decimal.RequireFromString("0.08")

subtotal := price.Mul(quantity)      // 59.97
tax := subtotal.Mul(rate)            // 4.7976
total := subtotal.Add(tax)           // 64.7676
payable := total.Round(2)             // 64.77

Decimal 是不可变值,AddSubMulDiv 都返回新值,不会修改原对象。

比较与格式化

不要把小数转换为浮点数后比较:

balance := decimal.RequireFromString("100.00")
amount := decimal.RequireFromString("25.50")

if balance.GreaterThanOrEqual(amount) {
    balance = balance.Sub(amount)
}

fmt.Println(balance.StringFixed(2)) // 74.50

常用比较方法包括 EqualLessThanLessThanOrEqualGreaterThanGreaterThanOrEqual

除法与舍入

除法可能产生无限小数,应根据业务规则明确精度和舍入方式:

amount := decimal.RequireFromString("10.00")

average := amount.DivRound(decimal.NewFromInt(3), 2) // 3.33
bankers := amount.Div(decimal.NewFromInt(3)).RoundBank(2)

金额分摊时,三份 3.33 的总和只有 9.99。应把余数分配给其中一份,而不是分别舍入后忽略差额。

存储建议

  • 数据库字段使用 DECIMALNUMERIC,不要使用浮点类型保存金额。
  • API 中可将金额序列化为字符串,避免 JavaScript 客户端再次产生精度损失。
  • 在业务边界统一精度和舍入规则,不要在展示层临时决定。
  • 高频数值计算需要先做性能测试;不可变 API 会比原地修改的数值类型产生更多分配。