参考
Money
go-sequent 的 types.Money:带 ISO 币种的 decimal 金额、JSON 用字符串编码、四则运算与舍入,以及用 AssertNoFloatMoney / BanFloat64Money 禁用 float64。
Money
包:types
types.Money 是 decimal 金额加上币种代码,也是框架唯一认可的金额类型。金额字段禁止使用 float64。
为什么用 decimal
二进制浮点无法精确表示 0.1;账本记几千笔之后,误差会悄悄累积。go-sequent 存的是精确的 decimal 金额,不以「整数最小单位」作为框架层的金额表示:你给多少精度就保留多少,怎么舍入由领域自己定。
decimal 运算来自 shopspring/decimal。
类型
type Money struct {
Amount decimal.Decimal `json:"amount"`
Currency string `json:"currency"`
}
从字符串构造:
m, err := types.NewMoney("100.00", "CNY")
m2 := types.MustMoney("25.50", "CNY") // 非法输入会 panic,适合测试夹具
z := types.Zero("CNY")
NewMoney 会去掉首尾空白、把币种转成大写;币种为空或金额非法时返回错误。
运算
| 方法 | 说明 |
|---|---|
Add(other)、Sub(other) |
返回 (Money, error);币种不同则报错。 |
Mul(factor decimal.Decimal) |
乘以一个 decimal 因子。 |
Round(places int32) |
按指定小数位舍入。 |
Cmp(other)、Equal(other) |
比较。币种不一致时 Cmp 报错。 |
IsNegative()、IsPositive()、IsZero() |
正负与零判断。 |
String() |
例如 100.00 CNY。 |
opening := types.MustMoney("100.00", "CNY")
next, err := opening.Sub(types.MustMoney("30.00", "CNY"))
if err != nil { /* 币种不一致 */ }
if next.IsNegative() { /* 余额不足 */ }
rounded := next.Round(2)
不同币种不会被悄悄加在一起。聚合应在应用事件前检查币种;bank 的 Account 就是这么做的。
JSON
Amount 以 decimal 字符串写出,绝不是 JSON 数字:
{"amount": "100.00", "currency": "CNY"}
这样中间层的 JSON 解析器不会把金额先变成浮点数。解码时优先认带引号的字符串;没有指数、且能解析成 decimal 的裸数字也能过,但不推荐。客户端请始终发字符串。币种缺失或为空会报错。
舍入与历史
go-sequent 不规定会计舍入规则——那是你的领域。事件里存精确金额,不要用浮点数去重算历史。规则变了就写新事件遵循新规则,旧事件保持原样。
禁用 float64
下面两个辅助函数会遍历一个值:若导出的 float64 字段名像金额(Amount、Balance、Money、Credit、Debit、Price、Fee、Total),就报错:
// 在测试中:
if err := testkit.BanFloat64Money(&bank.Deposit{}); err != nil { t.Fatal(err) }
// 在启动时:
if err := types.AssertNoFloatMoney(&bank.Deposit{}); err != nil { log.Fatal(err) }
对每个命令和事件类型都跑一遍。检查靠字段名,所以能抓住 Amount float64,但认不出叫 Cents、实际装金额的字段。把它当作安全网和代码评审约定即可。
type bad struct{ Amount float64 }
testkit.BanFloat64Money(bad{Amount: 1.23}) // 返回错误
相关
文档随 go-sequent 源码更新;若与 API 不一致,以源码为准。