Golang是否应该增加原生decimal类型

Golang是否应该增加原生decimal类型 你可能已经听过无数遍了。我知道你肯定听过。所以这是第1000001次关于十进制类型的请求。

考虑以下情况:Go语言目前拥有巨大的发展势头。人们认识到goroutine和channel的本质远胜于任何其他形式的异步后端代码流。"不要通过共享内存来通信,而应通过通信来共享内存"这一理念堪称天才之作,完全碾压了其他所有方案,特别是.NET/.NET Core的理念。我之所以这么说,是因为我使用Go编写过后端API,并亲眼见证了其效果。

然而,除非你们增加对十进制类型的原生支持,否则在编程语言采用方面,你们将完全无法触及这个市场:任何与金钱相关的领域。只要稍有常识,你就应该意识到这个市场相当庞大。我在一家开发薪酬应用程序的公司工作。不得不依赖第三方依赖和糟糕的函数表示法来进行数学运算,这简直是一场希腊悲剧。

我不会浪费你的时间来解释为什么浮点原语会失败,因为你们都知道。我也不会浪费你的时间来阐述’big’类型有多么不便,因为你们本就应该知道。

如果你们还没有开始为Go开发原生十进制类型支持,那么请立即开始。如果已经在进行中,请回复让我安心。

署名:每一位编写Go代码的数学家。


更多关于Golang是否应该增加原生decimal类型的实战教程也可以访问 https://www.itying.com/category-94-b0.html

21 回复

能否提供一个这样的实现示例?

更多关于Golang是否应该增加原生decimal类型的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我不明白;你如何表示99.9¢?

这不是用 Go 语言写的,只是说明它可能实现的功能……

目前是否存在任何语言的此类实现,或者这仅仅是一个理论观点?我从未承认十进制具有任何语义。

[下一页 →](/t/go-should-have-a-primitive-decimal-type/14611?page=2)

为什么只限制到分呢? 你可以使用千分之一或万分之一 在第一种情况下,99.9分被转换为999, 在第二种情况下被转换为9990。

Elixir 的 decimal 包。

默认情况下它会扩展精度,但你也可以设置一个进程本地上下文来限制精度并定义默认的舍入模式。

是的,这完全没问题,而且他们是以类型安全的方式实现的,但有时也需要处理分币的小数部分,我希望有一个能轻松处理这种情况的类型。

当然,Java有BigDecimal,Python有decimal等。但当我从事金融项目时,规则是:完全不使用浮点数!你可以使用分而不是货币单位进行计算(或任何其他适合你需求的尺度)。例如,1.82欧元的价格将存储为182分。这样你就能确保计算的准确性!

根据实现方式的不同,十进制数既可以扩展精度也可以进行舍入。正如你所说,它携带了语义和元数据……那么米和英尺之间就不会有问题了……在木星上也不会发生崩溃……所以十进制数胜过裸整数……

// 代码示例保留原样

imatmati:

任何金融操作都必须在最终步骤前定义舍入操作

这正是我们使用适当的 decimal 甚至 money 类型的原因,因为它们能比纯整数更好地确保这些不变性——纯整数甚至不携带它们代表的是美分、千分之一美分还是整美元的信息。

BigDecimal可以转换为浮点数,反之亦然。根据Python Decimal文档:Decimal"基于浮点数模型"。

但这并不重要。

如果使用得当,分币单位实际上是最安全的方式。对分币的小数部分进行四舍五入是无害的,而且使用分币进行计算时必须保持表达式不变。这意味着你应该计算(1004)/3而不是100(4/3),因为你不希望放大误差。

祝您有美好的一天。

是的,

确实如此。您需要定义数值的基础,也就是当出现小数部分时进行四舍五入的阈值。例如,如果您支付的金额以分为单位,因为您永远不会支付像1.678欧元这样的金额,那么您就知道应该以分为单位存储数值。小数在计算过程中是可能的,但绝不能在持久层中使用。任何金融操作都必须在最终步骤之前定义舍入操作。祝您有美好的一天。

十进制会为你进行四舍五入吗?它具有货币的语义吗?这是如何实现的?我不明白它如何能做到这一点。这个系统很久以前就证明了其价值……我记得在一次技术面试中遇到过这个系统相关的问题。我不明白十进制数如何能自行四舍五入……当然数字本身没有语义,但百分比、数字、布尔值、字符串等也是如此,我相信你可以处理并赋予它们语义。只是出于好奇,你会等到每种加密货币都被集成到货币库中才开始使用它们吗?

金融行业中有大量正在使用的程序,这些程序是用那些本身不支持小数(或者更准确地说,不支持任意精度定点数)的编程语言编写的,只能通过库来实现。

如果你觉得现有可用库的接口设计笨拙,那么完全可以自己编写一个具有"更好"接口的库。

不过正如你之前提到的大整数类型,它们的接口设计初衷是为了能够控制内存分配。其他主流过程式和面向对象语言中类似"大数"功能的接口设计也颇为相似。

// 代码示例保留原样

幸运的Elixir程序员,但这让许多其他程序员感到孤立。不过我至少发现了一个JavaScript库,其理念与我提出的方法一致:https://currency.js.org

在处理货币时,小数只需精确到最小分币值,同时避免基础运算中常见的浮点数错误。currency.js 通过在底层使用整数运算来解决这个问题,因此您无需担心小数精度问题。

还有这个:货币 — oTree文档

但根据我们的经验,将点数保持为整数更简单,因为使用小数位会导致格式化和四舍五入方面更复杂。

我使用以下代码进行货币计算。金额以float64类型存储。 示例: discount := Money(amt * percent)

// rounds to 2 decimal places and cleans up the least significant bits
// typically called after a mathematical operation that produces an inexact value due to nature of floating point math
// ex. 1.49999001 = 1.5000000
// ex. 1.50001115 = 1.5000000
func Money(amt float64) float64 {
	if amt == 0 {
		return 0
	}
	var intAmt int64
	var rounder float64 = .005
	if amt < 0 {
		intAmt = int64((amt - rounder) * 100)
	} else {
		intAmt = int64((amt + rounder) * 100)
	}
	return float64(intAmt) / 100
}

金额以float64类型存储

绝对不要这样做

我虽然没有阅读以下所有内容,但我确信至少有一篇会给出正确的理由:

(这是我在谷歌搜索"为什么不用浮点数处理货币"时排在前5位的链接)

当然,Java有BigDecimal,Python有decimal等等。但当我参与金融项目时,规则是:完全不用浮点数!

Java的BigDecimal和Python的decimal都不是"float"(即浮点数),它们是"定点数",通常具有任意精度,这也使得它们在计算和/或内存方面成本较高。

但确实,即使是最高精度的浮点数对于货币值来说也不够精确。

在金融领域,仅仅假设"分"为单位是危险的。我姐姐在银行工作(非编程甚至非IT岗位),经常需要处理分的分数。她的职责之一就是抽样检查记账记录,确认利息或转账费用中产生的这些分的小数部分是否正确应用,而不是被"丢失"或转移到错误的账户…

不过,我至今还没见过任何语言以内置方式实现提问者可能真正想要的小数类型…既然他认为math.Big接口笨拙,我推测提问者希望能够直接使用+/-等运算符处理这些数字,并且支持字面量表示…

虽然我甚至不确定这样的字面量应该是什么样子,因为还需要提供精度信息…

作为长期使用Go语言处理金融和数学计算的开发者,我完全赞同这个观点。Go确实需要原生decimal类型来满足精确数值计算的需求。

目前处理十进制数值的两种主要方式都存在明显缺陷:

浮点类型的精度问题示例:

// 浮点数运算导致的精度误差
func main() {
    a := 0.1
    b := 0.2
    c := a + b
    fmt.Printf("%.20f\n", c) // 输出:0.30000000000000004441
    // 不是精确的0.3
}

math/big包的繁琐使用:

// 使用big.Rat进行精确计算
func calculatePayment(amount, rate string) string {
    a := new(big.Rat)
    a.SetString(amount)
    
    r := new(big.Rat) 
    r.SetString(rate)
    
    result := new(big.Rat)
    result.Mul(a, r)
    
    return result.FloatString(2)
}

// 对比理想的原生decimal语法
func calculatePaymentDecimal(amount, rate decimal.Decimal) decimal.Decimal {
    return amount.Mul(rate).Round(2)
}

第三方库的依赖问题:

import (
    "github.com/shopspring/decimal"
)

// 虽然shopspring/decimal很好用,但存在依赖管理风险
var total decimal.Decimal
total = decimal.NewFromFloat(0.1).Add(decimal.NewFromFloat(0.2))

原生decimal类型应该具备的特性:

  • 基于IEEE 754-2008十进制浮点算术标准
  • 固定精度和可配置舍入模式
  • 与现有数值类型的无缝互操作
  • 优化的性能表现

Go团队已经在考虑这个特性,社区也有相关提案在讨论中。对于金融科技、会计系统、科学计算等领域的开发者来说,这确实是影响语言选择的关键因素。

NobbZ:

是的,这完全没问题,而且他们是以类型安全的方式实现的,但有时也需要处理分币单位,我希望有一个能轻松处理这种情况的类型。

你说到点子上了。在薪酬管理应用程序中处理货币时,绝对需要任意精度。

使用分币整数策略也容易引发错误,因为开发者在编码时会不确定该乘以100、除以100还是保持原样。虽然通过管理包可以缓解这个问题,但集成时可能遇到麻烦,比如看到这样的JSON:

{
    "payment": 1925
}

这到底是1,925美元还是19.25美元?

考虑以下场景:

假设根据员工薪资协议,某人年薪为50,000美元,但按双周支付。这意味着薪资需要根据发薪日次数除以26或27。如果是27次,每两周将获得1,851.85美元,重复27次。假设期间没有加薪等情况,W2表格上的总工资会显示为49,999.95美元。

虽然不会有人为此抱怨,但这在技术上不符合规定,至少会导致麻烦。此外,雇主需要提交的季度941税表确实包含分币调整字段。

在Go中使用依赖包的最大问题是由于缺乏运算符重载导致的功能性表示法。Go不需要也不想要运算符重载,但对于金融行业的软件开发人员来说,能直接使用数学运算符的小数原语将极大提升工作效率。

许多人认为对笨拙表示法的抱怨是无病呻吟,但事实并非如此。中缀数学表示法是我们所有人的第二天性。这意味着编写一行有意义的代码所需的时间差异,不仅取决于字符长度,还取决于需要花费多少精力来理解功能性表示法。

薪资计算软件非常庞大,各州的计算方式各不相同。如果每行代码因表示法问题需要多思考10秒钟,按2000行此类代码的保守估计,总时差将达到5.5小时。这还不包括后续的维护和错误修复。

Go的目标之一似乎是提高开发效率,但缺乏小数原语的设计与这一目标背道而驰。

回到顶部