Go 语言的浮点数精确性

Note

本文摘录自《Go语言趣学指南》第 6 章, 请访问 gpwgcn.com 以获取更多相关信息。

../_images/gpwgcn1.jpg

正如 0.33 只是 1/3 的近似值一样, 在数学上, 某些有理数是无法精确地用小数形式表示的。 那么自然地, 对近似值进行计算也将产生一个近似结果, 例如:

1/3 + 1/3 + 1/3 = 1

0.33 + 0.33 + 0.33 = 0.99

除了计算机硬件使用只包含 0 和 1 的二进制数而不使用包含 0~9 的十进制数来表示浮点数, 浮点数经常会受到舍入错误的影响。 例如, 计算机虽然可以精确地表示 1/3, 但是在使用这个数字和其他数字进行计算的时候会引发舍入错误, 就像代码清单 6-5 所示的那样。


代码清单 6-5 浮点数的不精确例子: float.go

third := 1.0 / 3.0 fmt.Println(third + third + third)    // 打印出“1”
piggyBank := 0.1
piggyBank += 0.2
fmt.Println(piggyBank)    // 打印出“0.30000000000000004”

正如所见, 浮点数也许并不是表示金钱的最佳选择。 解决这一问题的另一种做法是使用整数类型存储美分的数量, 下一章将会对此进行介绍。

退一步来讲, 因为我们的目标是存够去火星的旅行费, 所以对于 piggyBank 其实也没有必要锱铢必较, 把一两分钱看得那么重, 它只要能完成基本的储蓄功能就可以了。 为此, 我们可以让 Printf 函数只打印小数点后两位小数, 这样就可以简单快捷地把底层实现导致 的舍入错误掩盖掉。

正如代码清单 6-6 和代码清单 6-7 中的温度转换代码所示, 为了尽可能地减少舍入错误, 我们还可以将乘法计算放到除法计算的前面执行, 这种做法通常会得出更为精确的计算结果。


代码清单 6-6 先执行除法计算: rounding-error.go

celsius := 21.0
fmt.Print((celsius/5.0*9.0)+32, "o F\n")    // 打印出“69.80000000000001°F”
fmt.Print((9.0/5.0*celsius)+32, "o F\n")

代码清单 6-7 先执行乘法计算: temperature.go

celsius := 21.0
fahrenheit := (celsius * 9.0 / 5.0) + 32.0
fmt.Print(fahrenheit, "o F")    // 打印出“69.8°F”

注意, 在代码清单 6-5 中, piggyBank 变量的值是 0.30000000000000004 而不是我们想要的 0.30 。 在比较浮点数的时候, 必须小心:

piggyBank := 0.1
piggyBank += 0.2
fmt.Println(piggyBank == 0.3)    // 打印出“false”

为了避免上述问题, 我们可以另辟蹊径, 不直接比较两个浮点数, 而计算出它们之间的差, 然后通过判断这个差的绝对值是否足够小来判断两个浮点数是否相等。 为此, 我们可以使用 math 包提供的 Abs 函数来计算 float64 浮点数的绝对值:

fmt.Println(math.Abs(piggyBank-0.3) < 0.0001)    // 打印出“true”