Go Decimal 从 float64 构造小数为什么不能恢复原始十进制输入
来源:17golang原创
时间:2026-10-29 14:23:50 397浏览 收藏
Go Decimal 从 float64 构造小数,无法保证恢复原始十进制输入,因为原始文本转换成 float64 时,可能已经舍入成另一数值,且文本格式也被丢弃。decimal.NewFromFloat 接收到的只有这个浮点值,没有原始字符串。换一种数值容器,不能反推出已经丢失的信息。
官方地址:https://github.com/shopspring/decimal
这里讨论的是 shopspring/decimal。排查时要把两件事分开:新构造出的 Decimal 是否合理表示当前 float64,以及它是否等于用户最初输入的十进制数。前者成立,不代表后者必然成立。
先定位原始信息在哪一层丢失
一个 float64 只有有限的可表示数值。任意长度的十进制输入却没有这样的数量限制,因此不同输入可能落到同一个 float64 上。到了这一步,后续构造函数没有足够信息判断它原来是哪一个字符串。
另一类丢失与数值精度无关:1、1.0 和 1.00 在业务上可能表达不同录入精度,但转换成浮点数后不能靠数值本身区分。即使数值恰好能精确表示,也不代表原始书写形式被保留下来。

不要把简短显示当作恢复原始输入
NewFromFloat 的设计是选择可以转换回同一个 float64 的简短十进制表示。它并不承诺输出浮点数所储存二进制值的完整十进制展开,更不承诺找回之前的原始输入。因此 NewFromFloat(0.1) 得到简短的 0.1 是合理行为,不能据此证明每个 float64 都保存了它的原始十进制来源。
| 观察到的情况 | 能说明什么 | 不能说明什么 |
|---|---|---|
| 转换结果显示很短 | 该表示可用于当前浮点值的往返转换 | 原始输入未发生舍入 |
| 补足固定小数位 | 采用了指定显示格式 | 补出的位就是原始输入 |
| Decimal 与另一 Decimal 相等 | 当前两个十进制值相等 | 两者经历过完全相同的数据来源 |
| 原始文本仍然可用 | 可以从文本重新构造十进制数 | 可以忽略文本解析错误和业务范围 |
用相邻长小数区分两条构造路径
下面用两个靠近 1 的长小数作为说明样本。它们在十进制中不同,但差异小于这一位置附近的 float64 分辨能力,会舍入到同一个浮点值。直接用 NewFromString 则没有经过这个中间入口。
package main
import (
"fmt"
"log"
"strconv"
"github.com/shopspring/decimal"
)
func main() {
texts := []string{
"1.00000000000000001",
"1.00000000000000002",
}
for _, text := range texts {
// 直接解析十进制文本,避免先经过 float64。
exact, err := decimal.NewFromString(text)
if err != nil {
log.Fatal(err)
}
// 这条路径只用于对照已经引入的浮点舍入。
f, err := strconv.ParseFloat(text, 64)
if err != nil {
log.Fatal(err)
}
fromFloat := decimal.NewFromFloat(f)
fmt.Printf("input=%s direct=%s viaFloat=%s same=%t\n",
text, exact.String(), fromFloat.String(), exact.Equal(fromFloat))
}
// 固定两位小数的整数系数,必须连同指数一起约定。
quantity := decimal.New(7385, -2)
fmt.Println("quantity:", quantity.String())
}
按照数值表示规则,这两个样本经 float64 路径后都不能保留尾部差异;直接十进制路径仍表达各自的数值。代码供读者自行运行,文中没有提供本次实测输出,也没有把显示文本当作运行证据。选择这种长输入,是为了明确展示“不同输入合并成一个浮点值”,而不是仅展示一个看起来有很多小数位的结果。
示例的两条浮点解析路径都检查了错误。实际入口还要处理 NaN 和无穷值,因为 NewFromFloat 不接受这些特殊值,会触发 panic;不能因为函数没有 error 返回值,就把任意外部浮点输入直接送进去。
把修复放在输入边界
如果业务契约本来就是十进制文本,优先在入口保留字符串,再交给 NewFromString。若数据来自采用固定缩放单位的整数,则把整数系数和指数一同约定,例如示例中的 decimal.New(7385, -2) 表示 73.85。别让接口只给出 7385,却不说明其单位和缩放含义。

需要逐字回显或保留录入精度时,可把原始文本作为独立字段保存。Decimal 负责数值运算,原始字段负责来源与格式,这两个职责不应混在一起。即便直接解析字符串,也不要假定默认 String 方法会逐字还原前导零、尾零、正号和科学计数法等所有形式。
如果上游只提供 float64,当前系统只能接受这份已经确定的输入值,或请求上游调整协议。此时使用 Decimal 仍可让后续运算遵守十进制运算方式,但不能把它描述成恢复了上游原始精度。
舍入策略不能充当历史输入的恢复算法
业务规定保留两位小数,是一项新的取舍规则。Round 或指定指数的构造可以表达这种策略,但无法知道已经被舍掉的尾部究竟是多少。先格式化成字符串再调用 NewFromString,也只是从“格式化后的文本”构造,原始信息不会重新出现。
同样,已经在 float64 中完成的加减乘除,其结果不能通过最后包一层 Decimal 撤销。应当从需要十进制语义的那一段运算开始就使用 Decimal,同时独立规定除法精度和最终舍入位置;这里不把后续十进制运算泛称为永不舍入。
- 回到原始输入,确认是字符串、整数系数还是浮点值。
- 查看首次进入 float64 的位置,不只查看最后的 Decimal 构造。
- 区分数值精度和文本格式,分别选择保存方式。
- 将显示位数、业务舍入和来源恢复视为不同要求。
- 原文仍存在时重新从原文解析;原文不存在时明确上游输入限制。
NewFromFloat 本身就不能用吗?可以用。它适合把已有浮点值转换到 Decimal;问题在于是否错误地要求它恢复先前未知的输入。
先用 FormatFloat 再解析能恢复吗?不能。格式化函数收到的仍然是同一个 float64,无法知道已经丢失的尾数和文本形式。
增加 Decimal 的显示位数能补回精度吗?不能。显示策略只能呈现当前值,不会增加关于原始输入的信息。
-
199 收藏
-
300 收藏
-
329 收藏
-
494 收藏
-
422 收藏
-
446 收藏
-
402 收藏
-
496 收藏
-
397 收藏
-
389 收藏
-
476 收藏
-
138 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习