Go big.Rat 怎么从小数字符串保留精确值
来源:17golang原创
时间:2026-10-06 19:14:38 117浏览 收藏
Go 里要让小数字符串保留精确值,最直接的做法是把原始文本交给 new(big.Rat).SetString(s)。例如 "0.1" 会得到精确的 1/10,不要先用 strconv.ParseFloat 转成 float64,再调用 SetFloat64,因为那样保留下来的已经是二进制浮点近似值。
官方文档:https://pkg.go.dev/math/big
需要精确保存十进制输入时,用Rat.SetString;需要显示几位小数时,再在输出边界调用FloatString(位数)。存储与计算保持分数,舍入只发生在最终展示或结算规则明确的位置。
直接把小数字符串交给 SetString
我第一次在金额汇总代码里换用 big.Rat 时,最容易犯的错并不是 API 不会用,而是习惯性地先写 ParseFloat。这样虽然最后也得到一个 Rat,但它只是“精确保存了那个 float64”,并没有精确保存用户输入的十进制值。
正确的最小写法如下:
package main
import (
"fmt"
"math/big"
)
func main() {
// 直接解析原始十进制文本,避免中途经过 float64
value, ok := new(big.Rat).SetString("0.1")
if !ok {
// 解析失败时立即结束,不继续使用接收者
panic("无效的小数字符串")
}
// RatString 展示约分后的精确分数:1/10
fmt.Println(value.RatString())
// FloatString 只负责按指定小数位数展示:0.1000
fmt.Println(value.FloatString(4))
}
SetString 接受完整的小数、带指数的浮点形式以及 a/b 分数形式。官方文档强调:整个字符串都必须有效,不能只解析前缀。"12.345" 会被化为 2469/200,"1.25e-3" 也可以直接解析;但 "¥12.30"、"1,234.56" 或带空格说明文字的输入,应先在业务层完成清洗和格式规则判断。

封装解析错误并隔离失败对象
SetString 返回 (*Rat, bool)。失败时返回的 Rat 指针是 nil,而接收者的值处于未定义状态。对我来说,最稳妥的封装方式是每次创建一个新 Rat,只在 ok 为 true 时返回它:
package decimal
import (
"fmt"
"math/big"
"strings"
)
func Parse(s string) (*big.Rat, error) {
// 是否允许首尾空格应由业务规则决定,这里显式去除
normalized := strings.TrimSpace(s)
if normalized == "" {
return nil, fmt.Errorf("小数不能为空")
}
// 使用全新的接收者,解析失败后不把未定义值暴露出去
value := new(big.Rat)
if _, ok := value.SetString(normalized); !ok {
return nil, fmt.Errorf("无效的小数 %q", s)
}
return value, nil
}
这层封装还适合放业务限制,例如最多允许多少位小数、是否允许指数、是否允许负数。big.Rat 能解析的格式不等于业务都应接受。金额输入常常应拒绝指数形式,比例或科学数据则可能需要保留它。
全程用 Rat 完成加减乘除
解析后如果又把值转回 float64 做计算,前面的精确解析就失去意义。big.Rat 提供 Add、Sub、Mul 和 Quo,接收者保存结果。要特别注意:接收者会被覆盖。
package main
import (
"fmt"
"math/big"
)
func mustRat(s string) *big.Rat {
// 示例中集中处理解析失败,生产代码应返回 error
value, ok := new(big.Rat).SetString(s)
if !ok {
panic("无效小数: " + s)
}
return value
}
func main() {
price := mustRat("19.90")
taxRate := mustRat("0.075")
// 使用新接收者保存税额,避免覆盖 price 或 taxRate
tax := new(big.Rat).Mul(price, taxRate)
// total 同样使用独立对象,原始价格仍可复用
total := new(big.Rat).Add(price, tax)
// 结算展示在最终边界舍入到两位小数
fmt.Println(total.FloatString(2))
}
官方文档允许结果接收者与某个操作数是同一个对象,例如 sum.Add(sum, x)。这对累加很方便,但如果后面还需要原值,就应像示例一样用新的 big.Rat。复制 Rat 也不要做普通结构体浅拷贝,应调用 new(big.Rat).Set(source)。
除法还要先判断除数是否为 0,因为 Quo 在除数为 0 时会 panic。是否允许负数、零和极大的分子分母,也应在进入核心计算前由业务规则约束。
把精确存储与小数展示分开
big.Rat 保存的是数值,不保存输入格式。"1.2300" 与 "1.23" 都会归一化为同一个分数 123/100,末尾两个 0 不会被保留。因此需要区分三个目的:
| 目的 | 推荐方式 | 特点 |
|---|---|---|
| 保存精确数值 | RatString() 或分别保存 Num()/Denom() | 无展示舍入,可恢复原数值 |
| 固定小数位展示 | FloatString(prec) | 保留指定小数位,最后一位按最接近值舍入,正中间时远离 0 |
| 保留原始输入格式 | 额外保存原始字符串与 scale | 可区分 1.23 与 1.2300 |
我更愿意把 Rat 当成“计算值”,把币种、小数位数和舍入规则当成独立业务元数据。这样不会因为某个页面显示两位小数,就过早把所有中间结果都舍入成两位。
比较 Rat、float64、big.Float 和定点整数
这里没有万能类型。选择规则取决于你要保留的是十进制字面量、近似数值,还是固定比例的业务单位。

| 方案 | 适合 | 不适合 | 关键取舍 |
|---|---|---|---|
big.Rat.SetString | 精确十进制输入、比例、税率、可化为分数的运算 | 要求固定内存和极高吞吐的海量计算 | 值精确,但分子分母可能随运算增长 |
float64 | 统计、图形、测量值和允许误差的科学计算 | 必须精确相等的十进制业务值 | 快且通用,但很多十进制小数不能精确表示 |
big.Float | 需要自定义二进制精度与舍入模式的近似计算 | 想无损保留任意十进制字面量 | 精度可控,仍是二进制浮点模型 |
| 定点整数 | 币种最小单位明确、比例固定、范围可控 | 比例经常变化或分母不只含 2 和 5 的计算 | 简单高效,但要自行管理 scale 与溢出 |
最不推荐的路线是 字符串 → float64 → big.Rat。SetFloat64 的“exactly”是指精确表示传入的 float64,而不是恢复最初的十进制字符串。比如原始输入 0.1 在转成 float64 后已经是邻近的二进制值,再转 Rat 只会得到那个二进制值对应的精确大分数。
接入 JSON 和表单输入时保留原始文本
HTML 表单字段本来就是字符串,可以直接送入解析函数。JSON 更容易在解码阶段丢掉原始数值:若先解码到 float64,再转 Rat,问题仍然存在。可以把字段声明为字符串,或者让解码器使用 json.Number。
JSON 官方文档:https://pkg.go.dev/encoding/json
package main
import (
"encoding/json"
"fmt"
"math/big"
"strings"
)
func main() {
// UseNumber 让数字先保留为 json.Number 文本,而不是 float64
decoder := json.NewDecoder(strings.NewReader(`{"amount": 0.10}`))
decoder.UseNumber()
var payload struct {
Amount json.Number `json:"amount"`
}
if err := decoder.Decode(&payload); err != nil {
panic(err)
}
// 直接从 JSON 数字文本构造精确 Rat
amount, ok := new(big.Rat).SetString(payload.Amount.String())
if !ok {
panic("amount 不是有效小数")
}
fmt.Println(amount.RatString())
}
如果接口契约允许,我通常更倾向把金额、比例等精确值写成 JSON 字符串,并在服务端明确校验格式和小数位数。这比依赖不同语言对 JSON number 的默认类型更容易保持跨系统一致。
几个容易忽略的问题
SetString 会保留 19.90 的两个小数位吗?
不会。它保留数值 199/10,不保留输入的展示尺度。若业务必须记住两位小数,应单独保存 scale 或原始文本。
FloatString 会改变 Rat 本身吗?
不会。它只按指定小数位生成字符串。舍入发生在输出文本中,Rat 内部的精确分数不变。
什么时候定点整数比 big.Rat 更合适?
当单位和比例固定时,例如所有金额都以“分”为单位,而且数值范围能被 int64 覆盖,定点整数通常更简单、更快。涉及税率、比例链或任意分母时,Rat 更自然。
big.Rat 适合直接判断两个金额相等吗?
可以用 Cmp 比较数值。因为 Rat 会规范化分数,1.0 与 1.00 的数值相等;若展示尺度也是业务含义,则还要比较单独保存的 scale。
最终选择可以压缩成一句话:原始输入是十进制字符串且不能接受近似,就直接用 Rat.SetString;只在展示边界调用 FloatString,不要为了“方便”先经过 float64。如果业务规模、固定小数位或性能约束更重要,再在 big.Float 和定点整数之间做有条件的取舍。
-
151 收藏
-
101 收藏
-
323 收藏
-
428 收藏
-
143 收藏
-
Golang · Go教程 | 5小时前 | Go教程 · HTTP客户端 · 后端开发 · io.ReadAll io.LimitReader Go HTTP客户端 Go LimitedReader 响应体大小限制166 收藏
-
245 收藏
-
403 收藏
-
263 收藏
-
468 收藏
-
256 收藏
-
117 收藏
-
120 收藏
-
257 收藏
-
370 收藏
-
201 收藏
-
247 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习