golang高效处理ISO4217定点十进制货币运算插件库fpmoney的使用
Golang高效处理ISO4217定点十进制货币运算插件库fpmoney的使用
精确性很重要:使用浮点数表示货币几乎是犯罪。 — Robert.C.Martin,《代码整洁之道》第301页
特性
- 与
int64
一样快速 - 解析和打印时不使用
float
,不会丢失精度 - 支持
ISO 4217
标准货币 - 阻止不匹配货币的算术运算
- 仅100行代码
- 包含模糊测试
完整示例
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/nikolaydubina/fpmoney"
)
func main() {
// 创建SGD货币的固定价格
var BuySP500Price = fpmoney.FromInt(9000, fpmoney.SGD)
// JSON输入数据
input := []byte(`{"sp500": {"amount": 9000.02, "currency": "SGD"}}`)
// 定义结构体来解析JSON
type Stonks struct {
SP500 fpmoney.Amount `json:"sp500"`
}
var v Stonks
// 解析JSON
if err := json.Unmarshal(input, &v); err != nil {
log.Fatal(err)
}
// 初始化购买金额为0 SGD
amountToBuy := fpmoney.FromInt(0, fpmoney.SGD)
// 如果当前价格高于购买价格,则买入两倍金额
if v.SP500.GreaterThan(BuySP500Price) {
amountToBuy = amountToBuy.Add(v.SP500.Mul(2))
}
// 输出结果
fmt.Println(amountToBuy)
// 输出: 18000.04 SGD
}
极小分数处理
某些货币的面额分数非常小。使用int64
存储时:
BTC
的"聪"(satoshi)是1 BTC = 100,000,000 satoshi
,仍然足够存储约92,233,720,368 BTC
ETH
的"wei"是1 ETH = 1,000,000,000,000,000,000 wei
,只能存储约9 ETH
。如果需要处理wei,可能需要考虑使用bigint
或多个int64
性能基准测试
$ go test -bench=. -benchmem .
goos: darwin
goarch: arm64
pkg: github.com/nikolaydubina/fpmoney
cpu: Apple M3 Max
BenchmarkCurrency_UnmarshalText-16 711695404 1.610 ns/op 0 B/op 0 allocs/op
BenchmarkCurrency_AppendText-16 446232057 2.698 ns/op 0 B/op 0 allocs/op
BenchmarkCurrency_MarshalText-16 81956246 13.99 ns/op 8 B/op 1 allocs/op
BenchmarkCurrency_String-16 1000000000 1.064 ns/op 0 B/op 0 allocs/op
BenchmarkArithmetic/add-16 924924993 1.305 ns/op 0 B/op 0 allocs/op
BenchmarkJSON/small/encode-16 6004620 198.5 ns/op 160 B/op 3 allocs/op
BenchmarkJSON/small/decode-16 5047149 238.7 ns/op 152 B/op 2 allocs/op
BenchmarkJSON/large/encode-16 4739722 255.7 ns/op 176 B/op 3 allocs/op
BenchmarkJSON/large/decode-16 3737406 315.3 ns/op 152 B/op 2 allocs/op
BenchmarkBinary/small/encode-16 132380481 9.044 ns/op 16 B/op 1 allocs/op
BenchmarkBinary/small/decode-16 100000000 10.80 ns/op 16 B/op 1 allocs/op
BenchmarkBinary/large/encode-16 133549021 8.995 ns/op 16 B/op 1 allocs/op
BenchmarkBinary/large/decode-16 100000000 10.61 ns/op 16 B/op 1 allocs/op
PASS
ok github.com/nikolaydubina/fpmoney 15.804s
参考和相关工作
- ferdypruis/iso4217 是一个很好的灵感和参考材料
- github.com/shopspring/decimal: 固定精度;更快的打印/解析/算术运算;货币处理
- github.com/Rhymond/go-money: 解析时不使用
float
或interface{}
;货币是枚举 - github.com/ferdypruis/iso4217: 跳过已弃用的货币以适应
uint8
和更小的结构大小
更多关于golang高效处理ISO4217定点十进制货币运算插件库fpmoney的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html
1 回复
更多关于golang高效处理ISO4217定点十进制货币运算插件库fpmoney的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
fpmoney - Golang高效处理ISO4217货币运算的定点十进制库
fpmoney是一个专门为Golang设计的货币运算库,它使用定点十进制算术来处理符合ISO4217标准的货币计算,避免了浮点数运算带来的精度问题。
核心特性
- 基于int64的定点十进制表示
- 支持所有ISO4217标准货币
- 精确的加减乘除运算
- 货币格式化和解析
- 线程安全设计
安装
go get github.com/nikolaydubina/fpmoney
基本使用示例
创建货币金额
package main
import (
"fmt"
"github.com/nikolaydubina/fpmoney"
)
func main() {
// 创建100.50 USD
amount := fpmoney.FromInt(10050, fpmoney.USD)
// 或者从字符串创建
amountFromStr, err := fpmoney.FromString("100.50", fpmoney.USD)
if err != nil {
panic(err)
}
fmt.Println(amount.String()) // 输出: USD 100.50
fmt.Println(amountFromStr.String()) // 输出: USD 100.50
}
基本运算
// 加法
a := fpmoney.FromInt(10050, fpmoney.USD) // $100.50
b := fpmoney.FromInt(20025, fpmoney.USD) // $200.25
sum := a.Add(b)
fmt.Println(sum.String()) // 输出: USD 300.75
// 减法
diff := b.Sub(a)
fmt.Println(diff.String()) // 输出: USD 99.75
// 乘法 (乘以标量)
product := a.Mul(2)
fmt.Println(product.String()) // 输出: USD 201.00
// 除法 (除以标量)
quotient, err := a.Div(2)
if err != nil {
panic(err)
}
fmt.Println(quotient.String()) // 输出: USD 50.25
货币转换
// 假设汇率为1 USD = 0.85 EUR
usdAmount := fpmoney.FromInt(10000, fpmoney.USD) // $100.00
eurAmount := usdAmount.Convert(fpmoney.EUR, 85, 100) // 0.85 = 85/100
fmt.Println(eurAmount.String()) // 输出: EUR 85.00
比较操作
a := fpmoney.FromInt(10000, fpmoney.USD)
b := fpmoney.FromInt(10000, fpmoney.USD)
c := fpmoney.FromInt(20000, fpmoney.USD)
fmt.Println(a.Equal(b)) // true
fmt.Println(a.Less(c)) // true
fmt.Println(c.Greater(a)) // true
格式化输出
amount := fpmoney.FromInt(123456, fpmoney.USD)
// 默认格式
fmt.Println(amount.String()) // 输出: USD 1234.56
// 带货币符号的格式
fmt.Println(amount.Format()) // 输出: $1,234.56
// 自定义格式
fmt.Printf("%.2f %s\n", amount.Float64(), amount.Currency().Code) // 输出: 1234.56 USD
高级用法
处理不同货币
// 不同货币不能直接运算
usd := fpmoney.FromInt(10000, fpmoney.USD)
eur := fpmoney.FromInt(10000, fpmoney.EUR)
// 这会panic
// sum := usd.Add(eur)
序列化和反序列化
// 序列化为JSON
type Invoice struct {
Amount *fpmoney.Amount `json:"amount"`
}
invoice := Invoice{Amount: fpmoney.FromInt(10050, fpmoney.USD)}
data, err := json.Marshal(invoice)
if err != nil {
panic(err)
}
fmt.Println(string(data)) // 输出: {"amount":{"currency":"USD","value":10050}}
// 反序列化
var newInvoice Invoice
err = json.Unmarshal(data, &newInvoice)
if err != nil {
panic(err)
}
fmt.Println(newInvoice.Amount.String()) // 输出: USD 100.50
性能考虑
fpmoney使用int64作为底层存储,所有操作都是基于整数运算,因此性能很高。与使用big.Rat或float64的解决方案相比:
- 内存占用更小
- 运算速度更快
- 没有精度损失
最佳实践
- 始终使用fpmoney处理货币值,避免使用float64
- 尽早验证货币金额的有效性
- 在数据库存储时,考虑存储货币代码和值两个字段
- 对于需要高精度的场景,可以考虑使用更大的整数类型自行实现
fpmoney是处理金融计算的可靠选择,特别适合电子商务、银行系统和会计软件等需要精确货币计算的场景。