2.10 多精度浮点数与math/big库
尽管浮点数的表达和计算可能遇到精度问题,但是在一般场景下,这种轻微的损失基本可以忽略不计,Go语言内置的int64、float32类型可以满足大部分场景的需求。在一些比较特殊的场景下,例如加密、数据库、银行、外汇等领域需要更高精度的存储和计算时,可以使用math/big标准库,著名区块链项目以太坊即用该库来实现货币的存储和计算。math/big标准库提供了处理大数的三种数据类型——big.Int、big.Float、big.Rat,这些数据类型在Go语言编译时的常量计算中也被频繁用到。其中,big.Int用于处理大整数,其结构如下所示。
big.Int的核心思想是用uint切片来存储大整数,可以容纳的数据超过了int64的大小,甚至可以认为它是可以无限扩容的。
大数运算和普通int64相加或相乘不同的是,大数运算需要保留并处理进位。Go语言对大数运算进行了必要的加速,例如大整数乘法运算使用了Karatsuba算法。另外,执行运算时采用汇编代码。汇编代码与处理器架构有关,位于arith_$GOARCH.s文件中。如下例计算出第一个大于1099的斐波那契序列的值。在该示例中,使用big.NewInt函数初始化big.Int,使用big.Exp函数计算1099的大小,使用big.Cmp函数比较大整数的值,使用big.Add函数计算大整数的加法。
big.Float离不开大整数的计算,其结构如下。其中,prec代表存储数字的位数,neg代表符号位,mant代表大整数,exp代表指数。
big.Float的核心思想是把浮点数转换为大整数运算。举一个简单的例子,十进制数12.34可以表示为1234×10^-2,56.78可以表示为5678×10^2,那么有,12.34×56.78=1234×5678×10^-4,从而将浮点数的运算转换为了整数的运算。但是一般不能用uint64来模拟整数运算,因为整数运算存在溢出问题,因此big.Float仍然依赖大整数的运算。
需要注意的是,big.Float仍然会损失精度,因为有限的位数无法表达无限的小数。但是可以通过设置prec存储数字的位数来提高精度。prec设置得越大,其精度越高,但是相应地,在计算中花费的时间也越多,因此在实际中需要权衡prec的大小。当prec设置为53时,其精度与float64相同,而在Go编译时常量运算中,为了保证高精度,prec会被设置为数百位。
在上例中,x2,y2通过big.NewFloat初始化。当不设置prec时,其精度与float64相同。如上例中,x2,y2最终打印出的结果与x1,y1是完全一致的。
当把x2,y2的prec位数设置为100时,如下所示,可以看到,打印出的浮点数精度有明显的提升。
如果希望有理数计算不丢失精度,那么可以借助big.Rat实现。big.Rat仍然依赖大整数运算,其结构如下所示,其中,a、b分别代表分子和分母。
big.Rat的核心思想是将有理数的运算转换为分数的运算。例如12/34×56/78=(12×78+34×56)/(34×78),最后分子分母还需要进行约分。将有理数的运算转换为分数的运算将永远不会损失精度。对于下面这段程序,最终打印出的结果为z:1/3。
有一些第三方库采用了其他思路来处理浮点数的精度问题,例如笔者贡献过代码的shopspring/decimal[4]将浮点数以十进制的方式表示,其结构如下所示,即每个数值都表示为value×10exp。该库仍然是依靠封装big.Int实现的。其通过牺牲一定的效率换取更简单直观的API,并且该库在处理货币方面有一定优势,可以实现不丢失精度的计算。