金额格式化必须用 decimal.StringFixed,全程使用 shopspring/decimal.Decimal 类型存储计算,结合 ISO 4217 动态小数位、Webhook 安全解析及 x/text/message 本地化展示,确保精度贯穿全链路。

金额格式化必须用 decimal.StringFixed,别碰 fmt.Printf
Go 原生 float64 无法安全表示 0.1、0.01 等金额,直接格式化会引入不可控误差。比如 fmt.Sprintf("%.2f", 19.99) 在某些边界值下可能输出 "19.98" 或 "20.00" —— 这不是 bug,是 IEEE 754 浮点规则决定的。
正确做法是全程使用 shopspring/decimal.Decimal 类型存储和计算,最后用 StringFixed 输出:
amount := decimal.NewFromInt(1999).Div(decimal.NewFromInt(100)) // = 19.99 fmt.Println(amount.StringFixed(2)) // ✅ 稳定输出 "19.99"
-
StringFixed是四舍五入,不是截断;传2就一定保证两位小数,不足补零 - 不要先转
float64再格式化,那等于把精度问题又引回来了 - 前端传来的金额字符串(如
"19.99")应直接用decimal.NewFromString解析,别用strconv.ParseFloat
不同币种的小数位数得动态控制,不能硬写 StringFixed(2)
日元(JPY)、韩元(KRW)等货币法定无小数位,欧元(EUR)、美元(USD)强制两位,而有些加密货币支持 8 位。硬编码 StringFixed(2) 会导致日元显示成 "10000.00",违反当地金融规范。
推荐做法:查 ISO 4217 标准,建一张映射表:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
var currencyScale = map[string]int32{
"USD": 2, "EUR": 2, "GBP": 2,
"JPY": 0, "KRW": 0,
"BTC": 8, "ETH": 18,
}
- 从支付网关或用户 locale 获取币种代码(如
req.Currency),再查表取places - 查不到时 fallback 到 2,但要打 warning 日志,避免静默错误
- 注意 PayPal 和 Stripe 都要求金额以「最小单位整数」传入(如 USD 用分),返回时才按币种还原小数位
Webhook 回调里的金额字段必须用 decimal 解析,别信 JSON 中的数字
Stripe 和 PayPal 的 webhook payload 里,金额字段都是整数(如 "amount": 1999),但有些第三方支付网关会返回 "amount": 19.99 —— 这是非法的,但真实存在。如果用 json.Unmarshal 直接解到 float64 字段,就又掉进浮点陷阱。
- 定义 webhook struct 时,金额字段必须是
decimal.Decimal类型 - 实现自定义
UnmarshalJSON方法,优先尝试解析整数字符串("1999"),再 fallback 到浮点字符串("19.99"),最后才解析数字字面量 - PayPal v1 SDK 返回的
Amount字段名大小写不固定(有时是amount),建议用map[string]interface{}先取原始值,再转decimal
本地化显示货币符号和分隔符要用 golang.org/x/text/message
StringFixed 只管小数位,不管 ¥ 还是 $,也不管千分位是逗号还是空格。这些必须交给 message.Printer 处理,它能根据 language.Tag 自动适配。
p := message.NewPrinter(language.Japanese)
p.Printf("¥%s", amount.StringFixed(currencyScale["JPY"])) // → ¥10000
- 别拼接符号字符串(如
"$" + amount.StringFixed(2)),欧元区用户看到"€19.99"是对的,但巴西用户需要"R$ 19,99",符号位置和小数点符号都不同 - 千分位分隔符不能靠
strings.Replace手动加,message.Printer会按 locale 自动处理1000000 → "1,000,000"或"1.000.000" - 如果用
go-i18n做多语言文案,货币格式化仍要单独走x/text/message,它俩职责不同:一个管文案翻译,一个管数字/日期/货币格式
真正难的不是写对一行 StringFixed,而是让金额在计算、传输、存储、展示四个环节始终用同一套精度规则贯穿。任何一环退回到 float64 或字符串拼接,都会在某个深夜报警里重现。

















