很多人大概都知道整型变量可以直接(即按二进制位逐位比较)进行相等判断,但浮点型变量则不可以。原因很简单,在一定范围内,整数个数是有限的,所以整型可以在一定条件下表示所有整数。但对于实数,即使范围给定,其个数依旧是无穷的,所以无法用有限的二进制位数来一一对应某范围的所有实数。因而,实数在计算机里只能近似表示,也就是表示为我们熟知的(二进制)浮点数。如后文所述,其设计的特殊性决定了与整型变量之间的用法差异。
浮点数的设计,之前几乎每个处理器厂商都有自己的解决方案。但1985年出现的IEEE-754标准,因其简单易懂且易于实现,便成了最为广泛使用的浮点数运算标准,为许多处理器厂商所采用。
IEEE-754标准表示的二进制浮点数,包含三个部分:符号位、指数部分和尾数部分
那么,在golang语言中,浮点型具体又是怎么存储的呢?
浮点型结构
在Go语言编程中,用来表示小数的有两种类型:
- float32(单精度类型,占据4个字节 byte,32个二进制位 bit)
- float64(双精度类型,占据8个字节 byte,64个二进制位 bit)
我们知道,任何数据存储到计算机中都会变成0 1这样的二进制代码,只是不同类型的数据编码方式不同
对于浮点数,可以认为氛围两部分:整数和小数。对于整数部分来说,方式和int类型一样,从地位到高位,分别表示2的0次方 2^0、2^1、2^2 … … 等;对于小数部分来说,编码方式逻辑上一致,具体实现有所差异,从左到右每一位分别标识2的-1次方 2^-1、2^-2、2^-3 … …
首先,我们试着用二进制数据来表示一个小数:
二进制 | 十进制 | 计算方式 |
---|---|---|
3.5 | 11.1 | 2^0 + 2^0 + 2^-1 |
10.625 | 1010.101 | 2^3 + 2^1 + 2^-1 + 2^-3 |
0.6 | 0.10011001… | 2^-1 + 2^-4 + 2^-5 + 2^-8 + … |
转换成以 2 为底的科学计数法:
二进制 | 十进制 | 计算方式 |
---|---|---|
3.5 | 11.1 | 1.11 * 2^1 |
10.625 | 1010.101 | 1.010101 * 2^3 |
0.6 | 0.10011001… | 1.0011001… * 2^-1 |
从上面我们可以观察到,对于任何数来说,表示成二进制科学计数法后,都成以转换成 1.xxx(尾数) * 2 的 n 次方(指数)。
这里需要注意到的一点是,比如上图中的十进制小数0.6,表示成二进制后变成了以1001循环的无限循环小数。
这便是浮点数有精度问题的根源之一,在代码中声明的小数0.6,计算机底层其实是无法精确存储那个无限循环的二进制数的。
只能存入一个零舍一入(类似于十进制的四舍五入)后的近似值。
对于小于0的负数来说,则可以表示成 -1.xxx(尾数) * 2 的 n 次方(指数)
所以内存中要存储这个小数,按照 IEEE-754标准 分成三部分:
- 正负号
- 指数
- 尾数
如图所示:
具体存储方式如上图所示。最高位有1bit存储正负号,然后指数部分占据8bits(4字节)或11bits(8字节),其余部分全都用来存储尾数部分。
对于指数部分,这里存储的结果是实际的指数加上偏移量之后的结果。
这里设置偏移量,是为了让指数部分不出现负数,全都为大于等于0的正整数。
尾数部分的存储,因为二进制的科学计数法,小数点前一定是1开头,因此我们尾数只需要存储小数点后面的部分即可。
接下来依然是举例说明,4字节浮点数(Golang 中的 float32):
再来观察一个 8 字节浮点数(Golang 中的 float64)的例子:
偏移量:
- 字节浮点数的偏移量为 127
- 字节浮点数的偏移量为 1023
加上偏移量可以统一地把正数和负数统一转化成无符号的证书,方便进行比较,举例说明:
4字节浮点数的指数部分为 -7 ,则通常表示为 10000111 1为符号位,代表它是一个负数。
7 表示为 00000111 0为符号位,代表它是一个负数。
如果把 7 和 +7 统一加上偏移量 127
那么 7 就变成 134 ,二进制表示为 10000110
-7变成 120 ,二进制表示为 01111000
两者进行比较大小的时候,计算机便无需比较两者的符号位
具体事例
二进制表示
我们使用下面语句来打印float32浮点数对应的二进制表示
fmt.Printf("%b", math.Float32bits(data))
十进制 | 二进制 | 说明 |
---|---|---|
3 | 0 10000000 10000000 00000000 0000000 | 11.0 = 1.1*2^1 指数部分为127+1=128,尾数部分为1 |
1 | 0 01111111 00000000 00000000 0000000 | 1.0 = 1.0*2^0 指数部分为127,尾数部分为0 |
0.5 | 0 01111110 00000000 00000000 0000000 | 0.1 = 1.0*2^-1 指数部分为127-1=126,尾数部分为0 |
-0.25 | 1 01111101 00000000 00000000 0000000 | -0.01 = -1.0*2^-2 指数部分为127-2=125,尾数部分为0,符号位为1 |
问题描述
因为有些小数转换成二进制后会变成无限循环小数,所以存在精度问题
导致一个小数用float32和float64表示后,结果并不相等,比如: 0.3
fmt.Println(float64(float32(0.3)) == float64(0.3))
输出结果为 false
我们看看float32单精度表示,对应的二进制为 0 01111101 00110011 00110011 0011010
换成float64双精度,对应的二进制为0 01111111 10100110 01100110 01100110 01100110 01100110 01100110 0110011
先用单精度表示,然后强转成双精度,对应的二进制为0 01111111 10100110 01100110 01100110 10000000 00000000 00000000 0000000
可以看到,强转后的双精度和直接双精度结果并不一样,这是因为单精度只能保存32位信息,强转成64为,后面缺失的32位怎么办呢,这里会默认补0,所以会导致不一样
判断浮点数是否相等
像上面0.3的情况,肉眼可见认为是相等的,但实际跑出来的结果却不相等,这是由于计算机存储机制导致的
那我们怎么能准确判断两个浮点数是否相等呢?
提供两个思路
转换成string类型
func Decimal(value float32) string {
value1 := fmt.Sprintf("%.6f", value)
return value1
}
func Compare(val1,val2 string) bool {
indexf :=strings.Index(val1,".") + 4
indexs :=strings.Index(val2,".") + 4
if indexs != indexf {
return false
}else {
if val1[0:indexf] == val2[0:indexs]{
return true
}else {
return false
}
}
}
精度判断
package main
import (
"fmt"
"math"
)
const MIN = 0.000001
// MIN 为用户自定义的比较精度
func IsEqual(f1, f2 float64) bool {
if f1>f2{
return math.Dim(f1, f2) < MIN
}else{
return math.Dim(f2, f1) < MIN
}
}
func main() {
a := 0.9
b := 1.0
if IsEqual(a, b) {
fmt.Println("a==b")
}else{
fmt.Println("a!=b")
}
}
数据库中金额元存储的数据结构使用的是decimal(15,2),为了避免浮点型精度导致的问题,建议金额最好转换成最小单位(厘),用整型表示