十进制
Go 中的任意精度定点十进制数。
注意: Decimal 库“仅”能表示小数点后最多 2^31 位的数字。
特性
- 零值为 0,且无需初始化即可安全使用
- 加法、减法、乘法,无精度损失
- 指定精度的除法
- 数据库/sql 序列化/反序列化
- JSON 和 XML 序列化/反序列化
安装
运行 go get github.com/shopspring/decimal
要求
Decimal 库要求 Go 版本为 >=1.10
文档
http://godoc.org/github.com/shopspring/decimal
用法
package main
import (
"fmt"
"github.com/shopspring/decimal"
)
func main() {
price, err := decimal.NewFromString("136.02")
if err != nil {
panic(err)
}
quantity := decimal.NewFromInt(3)
fee, _ := decimal.NewFromString(".035")
taxRate, _ := decimal.NewFromString(".08875")
subtotal := price.Mul(quantity)
preTax := subtotal.Mul(fee.Add(decimal.NewFromFloat(1)))
total := preTax.Mul(taxRate.Add(decimal.NewFromFloat(1)))
fmt.Println("Subtotal:", subtotal) // Subtotal: 408.06
fmt.Println("Pre-tax:", preTax) // Pre-tax: 422.3421
fmt.Println("Taxes:", total.Sub(preTax)) // Taxes: 37.482861375
fmt.Println("Total:", total) // Total: 459.824961375
fmt.Println("Tax rate:", total.Sub(preTax).Div(preTax)) // Tax rate: 0.08875
}
替代库
在处理十进制数时,你可能会遇到本库并不完全适用的问题。
幸运的是,得益于出色的社区,我们有十几个其他库可供选择。
探索其他替代方案,找到最适合你需求的那一个 :)
- cockroachdb/apd - 任意精度,可变且 API 丰富,类似于
big.Int,性能优于本库 - alpacahq/alpacadecimal - 高性能,低精度(12 位),与本库完全兼容的 API
- govalues/decimal - 高性能,零分配,低精度(19 位)
- greatcloak/decimal - 专注于计费及电子商务 Web 应用相关用例的分支,包含开箱即用的 BSON 序列化支持
常见问题
为什么不直接使用 float64?
因为 float64(实际上任何二进制浮点类型)都无法精确表示
诸如 0.1 这样的数字。
考虑这段代码:http://play.golang.org/p/TQBd4yJe6B 你可能期望它
输出 10,但它实际输出的是 9.999999999999831。随着时间推移,
这些微小的误差会真正累积起来!
为什么不直接使用 big.Rat?
big.Rat 在表示有理数方面没问题,但 Decimal 更适合 表示货币。为什么?这里有一个(人为构造的)示例:
假设你使用 big.Rat,并且有两个数字 x 和 y,它们都
表示 1/3,并且你有 z = 1 - x - y = 1/3。如果你打印出
每一个,字符串输出必须在某处停止(为了简单起见,假设它在 3 位小数
处停止),所以你会得到 0.333、0.333 和 0.333。但是剩下的
0.001 去哪了?
以下是上述示例的代码:http://play.golang.org/p/lCZZs0w9KE
对于 Decimal,打印出的字符串精确地表示该数字。因此,
如果你有 x = y = 1/3(精度为 3),它们实际上将等于
0.333,并且当你执行 z = 1 - x - y 时,z 将等于 .334。没有钱
丢失!
你仍然需要小心。如果你想把一个数字 N 分成 3 份,你
不能只是把 N/3 发送给三个不同的人。你必须选择一个人发送
N - (2/3*N)。那个人将收到一分钱的零头。
但是,与 big.Rat 相比,使用 Decimal 更容易小心。
为什么 API 与 big.Int 的相似?
big.Int 的 API 旨在减少内存分配次数以实现最大 性能。这对于其使用场景来说是合理的,但代价是 API 笨拙且容易误用。
例如,要添加两个 big.Int,你执行:z := new(big.Int).Add(x, y)。一个
不熟悉此 API 的开发者可能会尝试执行 z := a.Add(a, b)。这
会修改 a 并将 z 设置为 a 的别名,这可能不是他们预期的。它
还会修改 a 的其他任何别名。
以下是使用 big.Int 的 API 可能引入的细微 bug 的示例: https://play.golang.org/p/x2R_78pa8r
相比之下,使用十进制数很难犯这类错误。十进制数
的行为与其他 go 数字类型类似:尽管 a = b 不会将
b 深拷贝到 a,但修改 Decimal 是不可能的,因为所有 Decimal 方法
都返回新的 Decimal,而不修改原始值。缺点是
这会导致额外的内存分配,因此 Decimal 的性能较低。 我的假设
是,如果你在使用 Decimal,你可能更关心正确性
而非性能。
License
MIT 许可证 (MIT)
这是 fpd.Decimal 的一个经过大量修改的分支,该分支同样以 MIT 许可证发布。