在公司项目中看到关于钱的数据在接口展示、添加修改的各种情况下结构体中相应字段用的都是string类型。觉得不太合理,因此查阅了一些资料,整理如下。
1、string还是float
在网上搜到一篇博客:
在程序计算中涉及到钱的时候,为什么要用Decimal而不是Float
博客中介绍,如果用float类型再转为decimal类型时会造成精度丢失。转为string后再转为decimal则可以解决问题,保证精度。看着好像是很完美的回答了我想要了解的问题,但仔细一看用的语言是python,于是我便亲自试了下:
Python 2.7.16 (default, Aug 30 2021, 14:43:11)
[GCC Apple LLVM 12.0.5 (clang-1205.0.19.59.6) [+internal-os, ptrauth-isa=deploy on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from decimal import Decimal
>>> a = 0.36
>>> b = str(a)
>>> b
'0.36'
>>> c = Decimal(a)
>>> c
Decimal('0.35999999999999998667732370449812151491641998291015625')
>>> d = Decimal(b)
>>> d
Decimal('0.36')
>>>
结果确实如此,但看版本是2.7,我怀疑是不是python2.7版本落后,因此存在问题,于是又换了python3,再次测试:
Python 3.8.2 (default, Jun 8 2021, 11:59:35)
[Clang 12.0.5 (clang-1205.0.22.11)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> from decimal import Decimal
>>> a = 0.36
>>> b = str(a)
>>> b
'0.36'
>>> c = Decimal(a)
>>> c
Decimal('0.35999999999999998667732370449812151491641998291015625')
>>> d = Decimal(b)
>>> d
Decimal('0.36')
结论依然是一样。好吧,证实了,对于python确实应该先转为string再转为decimal。
但是,python是这样,难道go也是这样吗,于是我又开始了进一步测试:
go中decimal使用第三方库
github.com/shopspring/decimal
package main
import (
"fmt"
"github.com/shopspring/decimal"
)
func main() {
var num float64 = 1
for num <100 {
rs := num/100+1
decimalRs := decimal.NewFromFloat(rs)
fmt.Println(rs,decimalRs)
//fmt.Printf("origin:%v,originType:%T,decimal:%v,decimalType:%T\n",rs,rs,decimalRs,decimalRs)
num +=1
}
}
这里是直接用float64转为decimal,并没有通过string中转,结果输出如下:
1.01 1.01
1.02 1.02
1.03 1.03
1.04 1.04
1.05 1.05
1.06 1.06
1.07 1.07
1.08 1.08
1.09 1.09
1.1 1.1
1.11 1.11
1.12 1.12
1.13 1.13
1.1400000000000001 1.1400000000000001
1.15 1.15
1.16 1.16
1.17 1.17
1.18 1.18
1.19 1.19
......
1.8900000000000001 1.8900000000000001
1.9 1.9
1.9100000000000001 1.9100000000000001
1.92 1.92
1.9300000000000002 1.9300000000000002
1.94 1.94
1.95 1.95
......
乍一看,看着好像是精度丢失了似的,但是对比后我们发现,float64和decimal前后都是一致的,也就是说float64转为decimal之后,保持了原样。那些看起来小数点后位数很多的结果,是因为float64浮点类型计算产生的问题,跟转换并没有关系。
为了验证这一结论,我又写了下面的测试:
package main
import (
"fmt"
"github.com/shopspring/decimal"
)
func main() {
var num float64 = 1
for num <100 {
rs := decimal.NewFromFloat(num/100).Add(decimal.NewFromInt(1))
fmt.Println(rs)
//fmt.Printf("origin:%v,originType:%T,decimal:%v,decimalType:%T\n",rs,rs,decimalRs,decimalRs)
num +=1
}
}
不使用浮点类型相加,而用decimal类型相加,这样输出结果果然没问题了:
1.01
1.02
1.03
1.04
1.05
1.06
1.07
1.08
1.09
1.1
1.11
1.12
1.13
1.14
1.15
1.16
1.17
......
因此我们可以得出结论:对于go语言,在把float转换为decimal类型时,并不需要像python一样通过string中转,也依然可以保持精度。进一步,我们也可以想到上面的问题,对于钱,在结构体字段中,我们完全可以直接使用float类型,并不需要用string类型。而且好处时,使用float数值类型,无论对于前端还是后端,在进行数值范围验证的时候,会更方便,不必把string转为float后再验证了。
2、decimal包的简要用法
上面提到了decimal包,我们这里简单看下,里面的常用的方法:
定义decimal的几种方式:
// 根据int64
func NewFromInt(value int64) Decimal {}
// 根据int32
func NewFromInt32(value int32) Decimal{}
// 根据string
func NewFromString(value string) (Decimal, error){}
// 根据float64
func NewFromFloat(value float64) Decimal {}
// 根据float32
func NewFromFloat32(value float32) Decimal {}
decimal数值运算
// 加。returns d + d2.
func (d Decimal) Add(d2 Decimal) Decimal {}.
// 减 returns d - d2.
func (d Decimal) Sub(d2 Decimal) Decimal {}
// 乘 returns d * d2.
func (d Decimal) Mul(d2 Decimal) Decimal {}
// 除 returns d / d2
func (d Decimal) Div(d2 Decimal) Decimal {}
// 求余 Mod returns d % d2.
func (d Decimal) Mod(d2 Decimal) Decimal {}
// 四舍五入保留2位小数,返回float64
decimal.NewFromFloat(d).Round(2).Float64()
比较:
// 比较 -1 if d < d2, 0 if d == d2, +1 if d > d2
func (d Decimal) Cmp(d2 Decimal){}
// 是否相等
func (d Decimal) Equal(d2 Decimal) bool {}
// 大于
func (d Decimal) GreaterThan(d2 Decimal) bool {}
// 大于等于
func (d Decimal) GreaterThanOrEqual(d2 Decimal) bool {}
// 小于
func (d Decimal) LessThan(d2 Decimal) bool {}
// 小于等于
func (d Decimal) LessThanOrEqual(d2 Decimal) bool {}
3、浮点精度(float、double)运算不精确的原因
我们再看下浮点精度运算不准确的原因,这篇文章讲解的比较细
浮点精度(float、double)运算不精确的原因
精度运算不准确这是为什么呢?我们接下来就需要从计算机所有数据的表现形式二进制说起了。如果大家很了解二进制与十进制的相互转换,那么就能轻易的知道精度运算不准确的问题原因是什么了。
接下来我们看一个简单的例子 2.1 这个十进制数转成二进制是什么样子的。
2.1 分成两部分
// 整数部分
2 / 2 = 1 .... 0
1 / 2 = 0 .... 1
// 小数部分
0.1 * 2 = 0.2 .... 0
0.2 * 2 = 0.4 .... 0
0.4 * 2 = 0.8 .... 0
0.8 * 2 = 1.6 .... 1
0.6 * 2 = 1.2 .... 1
0.2 * 2 = 0.4 .... 0
0.4 * 2 = 0.8 .... 0
0.8 * 2 = 1.6 .... 1
0.6 * 2 = 1.2 .... 1
0.2 * 2 = 0.4 .... 0
0.4 * 2 = 0.8 .... 0
0.8 * 2 = 1.6 .... 1
0.6 * 2 = 1.2 .... 1
............
落入无限循环结果为 10.0001100110011........ , 我们的计算机在存储小数时肯定是有长度限制的,所以会进行截取部分小数进行存储,从而导致计算机存储的数值只能是个大概的值,而不是精确的值。从这里看出来我们的计算机根本就无法使用二进制来精确的表示 2.1 这个十进制数字的值,连表示都无法精确表示出来,计算肯定是会出现问题的。
参考:
1、为什么数值表示用decimal 不用double
https://blog.csdn.net/a13766750034/article/details/101566632
2、在程序计算中涉及到钱的时候,为什么要用Decimal而不是Float
https://blog.csdn.net/weixin_43946756/article/details/87562619
3、MySQL数据类型DECIMAL用法
https://www.cnblogs.com/owenma/p/7097602.html
4、浮点精度(float、double)运算不精确的原因
https://blog.csdn.net/marco__/article/details/102515668