必须用 decimal.Decimal 替代 float64,从 JSON 字符串直解析、全程保持定点运算、按货币统一 scale、四舍五入选 RoundBank 或 RoundFloor、JSON 序列化加 ",string" 标签。

用 decimal.Decimal 替代 float64 是唯一靠谱做法
Go 原生 float64 无法精确表示 0.1、0.01 等十进制小数,汇率结算中 0.005 四舍五入偏差会直接导致资金差错。必须用定点数库——shopspring/decimal 是事实标准,它底层用整数存储「金额 × 10^scale」,完全规避浮点误差。
常见错误是先用 float64 解析 API 返回的 JSON 数值(比如 {"rate": 1.234567}),再转成 decimal.Decimal,此时精度已在解析阶段丢失。正确做法是:把原始字符串传给 decimal.NewFromString()。
- 从 JSON 读汇率时,字段类型声明为
json.RawMessage或string,避免自动转float64 - 构造
decimal.Decimal时统一用decimal.NewFromString("1.234567"),不用decimal.New(1234567, 6)手动拆分(易错) - 所有中间计算(如乘以金额、除以基准货币)全程保持
decimal.Decimal类型,不 cast 回 float
decimal.Decimal 的 RoundFloor 和 RoundBank 怎么选
汇率结算涉及四舍五入规则,不同国家/场景要求不同:RoundFloor 向下截断(保守计费),RoundBank 是银行家舍入(偶数优先,更公平),而默认的 RoundHalfUp 是常规四舍五入。
例如:将 USD 100 按汇率 0.827356 换算为 EUR,要求保留 2 位小数:
立即学习“go语言免费学习笔记(深入)”;
rate := decimal.NewFromString("0.827356")
amountUSD := decimal.NewFromString("100.00")
result := amountUSD.Mul(rate).RoundBank(2) // 得到 "82.74"- 跨境支付通常用
RoundBank,符合 ISO 4217 和多数央行规范 - 内部对账或风控扣减建议用
RoundFloor,避免多付 - 切勿依赖
fmt.Sprintf("%.2f", d)做格式化——它走 float 路径,会引入新误差
货币单位和 scale 必须显式对齐
不同货币最小单位不同:USD/EUR 最小单位是 0.01(scale=2),JPY 是 1(scale=0),KRW 也是 1,而 BTC 常用 8 位(scale=8)。混算时若未统一 scale,Mul 或 Add 会静默出错。
典型陷阱:把 JPY 10000(整数)和 USD 100.50(两位小数)直接相加,结果变成 10100.50 —— 实际应是 10000.00 + 100.50 = 10100.50,但若 JPY 存为 decimal.New(10000, 0),USD 为 decimal.New(10050, 2),两者 scale 不同,Add 会按更高 scale 对齐(即 JPY 变成 10000.00),结果正确;但若误把 USD 存为 decimal.New(10050, 0),就彻底错了。
- 初始化时明确指定 scale:
decimal.New(10050, 2)表示 100.50,decimal.New(10000, 0)表示 10000 JPY - 跨币种运算前,用
.Shift(2 - jpyScale)显式调整 scale(例如 JPY→EUR 需补两位小数) - 数据库存值推荐统一存为整数(如 cents),在应用层做 scale 转换,避免 DB 浮点类型污染
JSON 序列化时保留原始精度字符串
Go 默认用 json.Marshal 序列化 decimal.Decimal 会转成 float64 字面量(如 1.234567),前端或下游服务再解析又丢精度。必须自定义 JSON 编组行为。
最简方案:给结构体字段加 json:",string" 标签,让 decimal.Decimal 的 MarshalJSON 方法输出带引号的字符串:
type ExchangeResult struct {
Amount decimal.Decimal `json:"amount,string"`
Rate decimal.Decimal `json:"rate,string"`
}- 这样序列化后是
{"amount":"123.45","rate":"0.827356"},下游可安全解析 - 如果下游是 Java,注意 Jackson 默认不支持这种 string 包裹的 decimal,需配
@JsonDeserialize(using = DecimalDeserializer.class) - 别用
fmt.Sprintf拼接 JSON——绕过类型系统,极易漏掉小数末尾零(如 "100.00" → "100")
真正难的不是选哪个库或函数,而是整个数据流里每一步都得守住精度:从 HTTP body 解析、DB 查询、中间计算、缓存序列化,到最终返回。漏掉任意一环,前面的努力就白做了。


















