1
2
3
var f1 float64 = 0.1
var f2 float64 = 0.2
fmt.Println(f1 + f2)
上面这段简单的程序你以为会输出0.3,但实际输出的是0.30000000000000004。
我们知道计算机中的数据都是二进制存储的,问题也在于计算机中的小数是被表示成二进制存储而无限接近于实际值。
看一下十进制小数0.1转二进制的过程:
1
2
3
4
5
6
7
0.1\*2=0.2 取整数部分0,用0.2继续计算。
0.2\*2=0.4 取整数部分0,用0.4继续计算。
0.4\*2=0.8 取整数部分0,用0.8继续计算。
0.8\*2=1.6 取整数部分1,用0.6继续计算。
0.6\*2=1.2 取整数部分1,用0.2继续计算。
0.2\*2=0.4 取整数部分0,用0.4继续计算。
……
得到的整数依次是:0.0001100110011001……,可以发现表示成二进制后是无限循环小数。计算机中无法精确的存储无限循环的进制数,只能截取前n位表示近似值,这就是浮点数精度问题的根源。
Golang和其它语言(Python、Java、C++、C#…)一样使用了IEEE-754标准存储浮点数。对于任何数来说表示成二进制后,都成以转换成 1.xx * 2 的 n 次方。浮点数在内存中,可以拆为三部分存储:
- 符号位(1表示负数,0表示正数)
- 指数位(指数+偏移量)
- 小数位
精度 | 符号位 | 指数位 | 小数位 | 偏移量 |
---|---|---|---|---|
Single(32位) | 1[31] | 8[23-30] | 23[0-22] | 127 |
Double(64位) | 1[63] | 11[52-62] | 52[0-51] | 1023 |
我们仍以float32 0.1为例,看它在内存中是怎么存储的:
- 先将0.1用二进制浮点表示出来:即0.0001100110011001…
- 它为正数,则符号位为0。
- 它可以表示为 1.100110011001…*2^-4,偏移量为-4,指数位则为127-4=123,表示为二进制 01111011
- 拼接上面步骤中的尾数100110011001…(取23位,若位数不足则右侧用零补齐)
- 最终得到 0 01111011 10011001100110011001101
Golang中math包下提供了Float32bits、Float64bits函数可以直接查看浮点数用IEEE754规范表示的二进制:
1
2
3
4
fmt.Printf("%.32b\n", math.Float32bits(0.1))
// Output:
00111101110011001100110011001101
可见和我们上面计算的一致。