golang高效处理ISO4217定点十进制货币运算插件库fpmoney的使用

Golang高效处理ISO4217定点十进制货币运算插件库fpmoney的使用

fpmoney logo

精确性很重要:使用浮点数表示货币几乎是犯罪。 — 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

参考和相关工作


更多关于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标准的货币计算,避免了浮点数运算带来的精度问题。

核心特性

  1. 基于int64的定点十进制表示
  2. 支持所有ISO4217标准货币
  3. 精确的加减乘除运算
  4. 货币格式化和解析
  5. 线程安全设计

安装

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的解决方案相比:

  1. 内存占用更小
  2. 运算速度更快
  3. 没有精度损失

最佳实践

  1. 始终使用fpmoney处理货币值,避免使用float64
  2. 尽早验证货币金额的有效性
  3. 在数据库存储时,考虑存储货币代码和值两个字段
  4. 对于需要高精度的场景,可以考虑使用更大的整数类型自行实现

fpmoney是处理金融计算的可靠选择,特别适合电子商务、银行系统和会计软件等需要精确货币计算的场景。

回到顶部