在 Go 项目里,浮点数问题最容易在金额、对账和比较判断时暴露出来:看起来只是 0.1 + 0.2 这样的小计算,结果却可能把业务逻辑带偏。本文先解释误差为什么会出现,再逐项拆开几种常用处理方案,最后落到代码规范和数据库设计上,帮助你判断什么场景该用整数、什么场景该上高精度库,以及什么时候只能接受“带误差地比较”。
为什么 Go 里的浮点数会“不精确”
Go 的 float32 和 float64 遵循 IEEE 754 标准,本质上是用二进制科学计数法存储数值。问题在于,很多十进制小数并不能被有限位二进制精确表示,0.1 就是最常见的例子。
这和十进制里无法用有限小数精确表示 1/3 是同一个道理。也就是说,误差并不是在打印时突然出现的,而是在数值进入内存时就已经存在,后续参与运算后还会继续累积。
func main() {
a, b := 0.1, 0.2
fmt.Printf("0.1 + 0.2 = %.17fn", a+b)
fmt.Println("0.1 + 0.2 == 0.3 ?", a+b == 0.3)
}
输出结果:
0.1 + 0.2 = 0.30000000000000004
0.1 + 0.2 == 0.3 ? false
这类误差在演示程序里看起来只是多出几位尾数,但一旦进入支付、结算、分账、库存成本或统计分析场景,就可能变成条件判断失败、累计结果偏移、对账不一致等实际问题。
// 金融计算中的陷阱
price := 19.99
total := price * 3 // 期望59.97,实际59.970000000000006
因此,真正要解决的不是“怎么把输出变好看”,而是“业务核心值到底该不该用浮点数表示”。
四种解决方案,分别适合什么场景
用整数存储金额:金融场景的首选
如果处理的是金额,最稳妥的办法通常不是修补浮点误差,而是直接绕开它。常见做法是把金额统一换算成最小货币单位,例如“分”,然后用整数存储和计算。

// 推荐做法:用整数表示金额(单位:分)
type Money int64 // 单位:分
func (m Money) Yuan() float64 {
return float64(m) / 100.0
}
func (m Money) String() string {
return fmt.Sprintf("%.2f", m.Yuan())
}
func calculateTotal() {
price := Money(1999) // 19.99元 = 1999分
quantity := 3
total := price * Money(quantity)
fmt.Printf("总价: %s元n", total)
// 输出: 总价: 59.97元
}
这种方式的优点很直接:
- 计算过程完全避开浮点误差。
- 金额加减乘除的结果更可控,适合账务系统和支付系统。
- 和数据库中的精确数值字段配合时,边界更清晰。
它的前提也很明确:你得先定义好最小单位,以及业务是否允许用固定精度来表达。对绝大多数人民币金额场景来说,这通常是最值得优先考虑的方案。
使用 shopspring/decimal:业务代码里更直观
当场景需要保留十进制语义,又希望代码层面直接进行精确运算时,github.com/shopspring/decimal 是 Go 里很常见的选择。它尤其适合金额、费率、税额、折扣等不能容忍二进制浮点误差的业务字段。
import "github.com/shopspring/decimal"
func decimalCalculate() {
price := decimal.NewFromFloat(19.99)
quantity := decimal.NewFromInt(3)
total := price.Mul(quantity)
fmt.Printf("总价: %sn", total.String())
// 输出: 总价: 59.97
// 精确比较
result := decimal.NewFromFloat(0.1).Add(decimal.NewFromFloat(0.2))
target := decimal.NewFromFloat(0.3)
fmt.Println("0.1 + 0.2 == 0.3 ?", result.Equal(target))
// 输出: true
}
这类库的优势在于:
- 支持任意精度计算。
- 提供更完整的四则运算与比较方法。
- 在业务代码里表达更自然,阅读成本通常低于自己封装一套金额类型。
当然,代价是需要引入第三方依赖。对于依赖治理严格、运行环境受限或基础设施强调标准库闭环的团队,这一点需要提前评估。
使用 math/big:不引依赖时的高精度方案
如果你希望保留高精度能力,又不想引入第三方包,那么 Go 标准库中的 math/big 是直接可用的选择。它提供了 big.Float、big.Int、big.Rat 等类型,适合对精度要求高、同时又强调依赖可控的项目。
import "math/big"
func bigCalculate() {
// 使用big.Float进行高精度计算
price := new(big.Float).SetFloat64(19.99)
quantity := new(big.Float).SetInt64(3)
total := new(big.Float).Mul(price, quantity)
fmt.Printf("总价: %sn", total.String())
// 输出: 总价: 59.97
// 精确比较
a := new(big.Float).SetFloat64(0.1)
b := new(big.Float).SetFloat64(0.2)
result := new(big.Float).Add(a, b)
target := new(big.Float).SetFloat64(0.3)
fmt.Println("0.1 + 0.2 == 0.3 ?", result.Cmp(target) == 0)
// 输出: true
}
math/big 的优势主要有三点:
- 标准库自带,无需外部依赖。
- 支持任意精度浮点数和有理数。
- 适合底层组件、通用库或对供应链管理要求较高的环境。
不过,从日常业务开发体验来看,math/big 的 API 相对底层,使用时需要更清楚地理解对象创建、比较方式和精度控制,写起来也通常没有 decimal 那么顺手。
继续使用浮点数时,至少要做误差比较
并不是所有场景都必须追求十进制精确表示。科学计算、图形处理、传感器数据分析、普通统计值等场景,使用 float64 往往仍然是合理选择。这时最关键的一条规则是:不要直接比较两个浮点数是否完全相等。
import "math"
const epsilon = 1e-9
func FloatEqual(a, b float64) bool {
return math.Abs(a-b) < epsilon
}
func compareFloat() {
a := 0.1 + 0.2
b := 0.3
fmt.Println("直接比较:", a == b)
fmt.Println("误差比较:", FloatEqual(a, b))
}
这里的关键不只是写一个 FloatEqual,而是理解 epsilon 不能照抄。误差阈值必须结合量级、业务容忍度和数据来源来设定:
- 阈值过大,可能把本来不同的结果误判为相同。
- 阈值过小,则可能依旧挡不住浮点误差。
所以这类方案更适合“允许近似值”的计算问题,而不适合账务、结算、积分扣减这类必须精确一致的结果判断。
落到工程实践:类型选择、代码规范与数据库设计
先按场景选类型,不要统一套用一种写法
浮点数问题真正难的地方,不在于有没有工具,而在于很多项目一开始就把“金额”“统计值”“科学数据”全塞进了同一种数值类型里。更稳妥的做法,是按场景分开处理:

| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 金融金额 | 整数(分)或decimal | 确保精确计算 |
| 高精度计算 | math/big或decimal | 无第三方依赖选big |
| 科学计算 | float64 + 误差控制 | 平衡精度与性能 |
| 一般业务 | float64 | 注意避免直接比较 |
这张表背后的判断标准其实很简单:是否要求结果精确一致,是否强调十进制语义,以及是否需要控制依赖。
代码层面最容易踩的两个坑
很多线上问题并不是因为团队完全不知道浮点数会有误差,而是因为它常常以“看起来很自然”的写法混进代码里。下面两类情况尤其常见。
// ❌ 错误:直接比较浮点数
if balance == 100.0 {
// ...
}
// ✅ 正确:使用误差比较,或使用decimal库
if math.Abs(balance-100.0) < epsilon {
// ...
}
// ❌ 错误:浮点数累加
var total float64
for _, item := range items {
total += item.price
}
// ✅ 正确:使用decimal累加
total := decimal.Zero
for _, item := range items {
total = total.Add(item.price)
}
第一类是直接比较,第二类是循环累加。前者容易让判断条件失效,后者则会在长链路累计中不断放大误差。如果这些逻辑刚好处于订单汇总、账户余额、结算明细等敏感流程,问题就会非常难排查。
数据库里也别用 FLOAT/DOUBLE 存金额
应用代码修正了精度问题,并不代表整个链路就安全了。如果数据库层仍然把金额字段设计成 FLOAT 或 DOUBLE,误差仍然可能在入库、查询、聚合和回写过程中重新出现。
对于金额字段,更稳妥的设计是使用 DECIMAL:
-- MySQL示例
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
amount DECIMAL(19, 4) NOT NULL, -- 精确存储
-- ...
);
Go 侧也应使用能正确承接精确值的类型来映射:
// 使用decimal库处理数据库DECIMAL类型
type Order struct {
ID int64
Amount decimal.Decimal `gorm:"type:decimal(19,4)"`
}
换句话说,金额精度不是某一行 Go 代码的问题,而是一个从内存表示、业务计算到数据库存储都要统一口径的工程问题。
结论:涉及金钱时,不要把希望押在浮点数上
浮点数精度问题是计算机中的基础事实,不是 Go 特有的“坑”。真正重要的是,你是否在设计阶段就区分了“近似计算”和“精确计算”这两类需求。
如果是金额、对账、结算一类业务,优先考虑整数化存储,或直接使用 decimal;如果要高精度但不想引入外部依赖,可以选 math/big;如果场景允许近似值,至少要使用误差范围而不是直接 == 比较。
记住一条最实用的原则:涉及金钱的计算,永远不要使用浮点数。










