登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

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 的显示位数能补回精度吗?不能。显示策略只能呈现当前值,不会增加关于原始输入的信息。

参考:Decimal 构造函数的官方说明与实现、Go 数值类型规范。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>