背景:
the actual total amount of batch 1191 is 58508228.05 but the Total Transaction Amount shows in admin is 58508228
数据库存储amount这个字段用的数据类型是decimal(20,4),现在需要计算的是sum(amount),并将其取出显示在admin上,由于sum(amount)的值可能较大,而golang语言中没有decimal这个数据类型,因此必然涉及到数据类型转换问题
bug来源:
一开始的思路:decimal → string → float32
record.Amount, err = strconv.ParseFloat(string(v["sum(amount)"]), 32)
浮点类型是不精确的类型,即便是float64也无法精确反映数据本身
原因:对于二进制小数,小数点右边能表达的值是 1/2, 1/4, 1/8, 1/16, 1/32, 1/64, 1/128 … 1/(2^n),也就是说计算机只能用这些 1/(2^n)之和来表达十进制的小数
例如:计算机表达十进制的 0.2 :
0.01 = 1/4 = 0.25 ,太大
0.001 =1/8 = 0.125 , 又太小
0.0011 = 1/8 + 1/16 = 0.1875 , 逼近0.2了
0.00111 = 1/8 + 1/16 + 1/32 = 0.21875 , 又大了
0.001101 = 1/8+ 1/16 + 1/64 = 0.203125 还是大
0.0011001 = 1/8 + 1/16 + 1/128 = 0.1953125 这结果不错
0.00110011 = 1/8+1/16+1/128+1/256 = 0.19921875
已经很逼近了, 就这样吧
而float32和float64的区别就在于,计算机提供的二进制位数不同,因为计算机不可能无限逼近,那么位数的作用就是在有限的位数内不断逼近十进制数
解决方案:
为了提供精确的值,最后选择"github.com/shopspring/decimal"
这个包,decimal.NewFromString之所以可以精确转换小数,是因为它结合了big.int以及进制的巧妙精准转换
代码示例:
package main
import (
"fmt"
"github.com/shopspring/decimal"
"strconv"
)
func main() {
test01, err := strconv.ParseFloat("3840629.82", 32)
test02, err := strconv.ParseFloat("3840629.82", 64)
test03, err := decimal.NewFromString("3840629.82")
test04, _ := test03.Float64()
if err != nil {
panic(err)
}
fmt.Printf("%f\n", test01)
fmt.Printf("%f\n", test02)
fmt.Printf("%v\n", test03)
fmt.Printf("%f\n", test04)
}
/* 输出:
3840629.750000
3840629.820000
3840629.82
3840629.820000
*/