一架梯子,一头程序猿,仰望星空!

Go Decimal 包,解决浮点数计算防止精度丢失问题。

Go Decimal库是一个在Go语言中处理任意精度定点小数的强大工具。它允许进行加法、减法、乘法和除法运算,而不会丢失精度。

1. 简介

Go Decimal库是一个在Go语言中处理任意精度定点小数的强大工具。它允许进行加法、减法、乘法和除法运算,而不会丢失精度。此外,它还提供了诸如数据库/SQL序列化/反序列化以及JSON/XML序列化/反序列化等功能。

2. 安装

要安装Go Decimal库,可以使用以下命令:

go get github.com/shopspring/decimal

请注意,Decimal库要求Go版本 >=1.7。

3. 基本用法

要在Go程序中使用Decimal库,导入"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
}

4. 创建Decimal变量

Decimal库提供了多种方法来创建Decimal变量。以下是支持的API:

  • decimal.NewFromBigInt(value *big.Int, exp int32) Decimal
  • decimal.NewFromFloat(value float64) Decimal
  • decimal.NewFromFloat32(value float32) Decimal
  • decimal.NewFromFloatWithExponent(value float64, exp int32) Decimal
  • decimal.NewFromFormattedString(value string, replRegexp *regexp.Regexp) (Decimal, error)
  • decimal.NewFromInt(value int64) Decimal
  • decimal.NewFromInt32(value int32) Decimal
  • decimal.NewFromString(value string) (Decimal, error)
  • decimal.RequireFromString(value string) Decimal

5. 算术操作

Go Decimal库提供了几种可以对Decimal变量进行的算术操作。以下是一些支持的操作:

  • Add(d2 Decimal) Decimal:将两个Decimal值相加并返回结果。
  • Sub(d2 Decimal) Decimal:从一个Decimal值中减去另一个Decimal值并返回结果。
  • Div(d2 Decimal) Decimal:将一个Decimal值除以另一个Decimal值并返回结果。
  • DivRound(d2 Decimal, precision int32) Decimal:将一个Decimal值除以另一个Decimal值,并指定精度返回结果。
  • Mod(d2 Decimal) Decimal:计算一个Decimal值除以另一个Decimal值的模(余数)并返回结果。
  • Mul(d2 Decimal) Decimal:将两个Decimal值相乘并返回结果。

您可以使用这些操作对Decimal值进行常见的算术运算。以下是一个演示使用这些操作的示例:

price, _ := decimal.NewFromString("136.02")
quantity := decimal.NewFromInt(3)

subtotal := price.Mul(quantity)
tax := subtotal.Mul(decimal.NewFromFloat(0.08875))

total := subtotal.Add(tax)

fmt.Println("Subtotal:", subtotal) // Subtotal: 408.06
fmt.Println("Tax:", tax)           // Tax: 36.244985
fmt.Println("Total:", total)       // Total: 444.304985

在上面的示例中,我们通过使用Mul()方法将pricequantity相乘来计算subtotal。然后我们通过将subtotal和税率相乘来计算tax。最后,我们通过使用Add()方法将subtotaltax相加来计算total

6. 四舍五入操作

Go Decimal库提供了几种四舍五入操作,可以用于将Decimal值舍入到指定的精度。以下是一些可用的四舍五入操作:

  • Round(places int32) Decimal:将小数舍入到指定的小数位数。
  • RoundBank(places int32) Decimal:使用银行家舍入法将小数舍入到指定的小数位数。
  • RoundCash(interval uint8) Decimal:将小数舍入到特定的间隔,如5分、10分、25分、50分或1元。
  • RoundCeil(places int32) Decimal:向正无穷舍入小数。
  • RoundDown(places int32) Decimal:向零方向舍入小数。
  • RoundFloor(places int32) Decimal:向负无穷舍入小数。
  • RoundUp(places int32) Decimal:朝离零较远的方向舍入小数。

6.1. Round

Round将小数舍入到places位小数。如果places < 0,则会将整数部分舍入到最近的10^(-places)。

NewFromFloat(5.45).Round(1).String() // 输出:"5.5"
NewFromFloat(545).Round(-1).String() // 输出:"550"

6.2. RoundBank

RoundBank将小数舍入到places位小数。如果要舍入的最后一位小数距离最近的两个整数的距离相等,则舍入值取偶数。

如果places < 0,则会将整数部分舍入到最近的10^(-places)。

NewFromFloat(5.45).RoundBank(1).String() // 输出:"5.4"
NewFromFloat(545).RoundBank(-1).String() // 输出:"540"
NewFromFloat(5.46).RoundBank(1).String() // 输出:"5.5"
NewFromFloat(546).RoundBank(-1).String() // 输出:"550"
NewFromFloat(5.55).RoundBank(1).String() // 输出:"5.6"
NewFromFloat(555).RoundBank(-1).String() // 输出:"560"

6.3. RoundCash

RoundCash(又称为现金/分/爱尔兰枚的舍入)将小数舍入到特定的间隔上。现金交易的应付金额将舍入为最接近的最小货币单位的倍数。可用的间隔有:5、10、25、50和100;其他任何数字都会抛出异常。

  5:   5分舍入 3.43 => 3.45
 10:  10分舍入 3.45 => 3.50(5被舍入上去)
 25:  25分舍入 3.41 => 3.50
 50:  50分舍入 3.75 => 4.00
100: 100分舍入 3.50 => 4.00

6.4. RoundCeil

RoundCeil向正无穷方向舍入小数。

NewFromFloat(545).RoundCeil(-2).String()   // 输出:"600"
NewFromFloat(500).RoundCeil(-2).String()   // 输出:"500"
NewFromFloat(1.1001).RoundCeil(2).String() // 输出:"1.11"
NewFromFloat(-1.454).RoundCeil(1).String() // 输出:"-1.5"

6.5. RoundDown

RoundDown向零方向舍入小数。

NewFromFloat(545).RoundDown(-2).String()   // 输出:"500"
NewFromFloat(-500).RoundDown(-2).String()   // 输出:"-500"
NewFromFloat(1.1001).RoundDown(2).String() // 输出:"1.1"
NewFromFloat(-1.454).RoundDown(1).String() // 输出:"-1.5"

6.6. RoundFloor

RoundFloor向负无穷方向舍入小数。

NewFromFloat(545).RoundFloor(-2).String()   // 输出:"500"
NewFromFloat(-500).RoundFloor(-2).String()   // 输出:"-500"
NewFromFloat(1.1001).RoundFloor(2).String() // 输出:"1.1"
NewFromFloat(-1.454).RoundFloor(1).String() // 输出:"-1.4"

6.7. 向上取整

RoundUp 向远离零的方向舍入十进制数。

NewFromFloat(545).RoundUp(-2).String()   // 输出: "600"
NewFromFloat(500).RoundUp(-2).String()   // 输出: "500"
NewFromFloat(1.1001).RoundUp(2).String() // 输出: "1.11"
NewFromFloat(-1.454).RoundUp(1).String() // 输出: "-1.4"

7. Decimal类型转换为字符串

Go Decimal 库提供了将Decimal数值转换为字符串表示的方法,这里列举了一些可用的方法:

  • String(): string:返回带有固定小数点的十进制数的字符串表示。
  • StringFixed(places int32) string:返回指定小数位数的四舍五入的固定小数点字符串表示。
  • StringFixedBank(places int32) string:返回指定小数位数的四舍五入(银行家舍入)的固定小数点字符串表示。

您可以根据需求选择相应的方法。下面是一个将十进制数转换为字符串的示例:

d := decimal.NewFromFloat(5.45)
str := d.String()

fmt.Println("十进制数的字符串表示:", str) // 十进制数的字符串表示: 5.45

// StringFixed 示例
NewFromFloat(0).StringFixed(2) // 输出: "0.00"
NewFromFloat(0).StringFixed(0) // 输出: "0"
NewFromFloat(5.45).StringFixed(0) // 输出: "5"
NewFromFloat(5.45).StringFixed(1) // 输出: "5.5"
NewFromFloat(5.45).StringFixed(2) // 输出: "5.45"
NewFromFloat(5.45).StringFixed(3) // 输出: "5.450"
NewFromFloat(545).StringFixed(-1) // 输出: "550"

// StringFixedBank 示例
NewFromFloat(0).StringFixedBank(2) // 输出: "0.00"
NewFromFloat(0).StringFixedBank(0) // 输出: "0"
NewFromFloat(5.45).StringFixedBank(0) // 输出: "5"
NewFromFloat(5.45).StringFixedBank(1) // 输出: "5.4"
NewFromFloat(5.45).StringFixedBank(2) // 输出: "5.45"
NewFromFloat(5.45).StringFixedBank(3) // 输出: "5.450"
NewFromFloat(545).StringFixedBank(-1) // 输出: "540"

8. 常见问题

问: 为什么不直接使用 float64? 答: float64 无法准确地表示像 0.1 这样的数字。它可能导致小的误差,在处理财务计算等涉及金额的情况下,这些误差可能会随着时间的推移累积并导致重大问题。

问: 为什么不直接使用 big.Rat? 答: 虽然 big.Rat 可以表示有理数,但对于表示货币来说并不适用。Decimal数在财务计算中更好,因为它可以准确地表示小数分数而不会丢失精度。

问: 为什么 API 不和 big.Int 的类似? 答: Decimal 库的 API 更注重易用性和正确性而非性能。虽然 big.Int 的 API 为了性能原因减少了内存分配,但这可能导致复杂且容易出错的代码。Decimal 库的 API 被设计为简单易懂。

章节目录