前言
先从一个strconv.FormatFloat
的例子说起,你知道以下输出的结果吗?
fmt.Println(strconv.FormatFloat(0.115, 'f', 2, 64))
fmt.Println(strconv.FormatFloat(0.125, 'f', 2, 64))
fmt.Println(strconv.FormatFloat(0.1250, 'f', 2, 64))
fmt.Println(strconv.FormatFloat(0.1251, 'f', 2, 64))
输出的结果依次是:
0.12
0.12
0.12
0.13
从结果来看,strconv.FormatFloat
并不完全符合四舍五入,所以其控制精度的规则是什么呢?
更多内容分享,欢迎关注公众号:Go开发笔记
小数的二进制表示问题
我们知道,当前计算机几乎只能处理二进制的数据,非二进制数据需要转为二进制才能处理。才能进行。一个十进制的整数,可以通过除2取余,逆序排列
(用2去除十进制整数,可以得到一个商和余数;再用2去除商,又会得到一个商和余数,如此进行,直到商为零时为止,然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来)的方式得到其二进制形式。
那么十进制的小数怎么转成二进制?
与整数的转换方式相反,乘2取整,顺序排列
(用2乘十进制小数,得到积,将积的整数部分取出,再用2乘余下的小数部分,得到积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,或者达到所要求的精度为止。然后把取出的整数部分按顺序排列起来,先取的整数作为二进制小数的高位有效位,后取的整数作为低位有效位)即可得到小数部分的二进制形式。
不同的是,除2取余,必然可以结束,即整数的二进制必然是精确的
。乘2则不一定,比如0.2,其可以一直进行下去。也就是说,小数的二进制表示有时是不可能精确的
。目前表示小数的类型有float32
和float64
,均只能保存一定长度的数据。因此,只能截取有限的部分保存。
根据小数的二进制
的计算方式,可以知道小数末尾的0
对结果无影响。
根据IEEE 754的规定,一个浮点数值可以由以下三部分表示:
符号 指数部分 尾数部分
go中float的结构如下:
type floatInfo struct {
mantbits uint
expbits uint
bias int
}
var float32info = floatInfo{23, 8, -127}
var float64info = floatInfo{52, 11, -1023}
因此,float32
只能保存23位,float64
只能保存52位。考虑到数据转换回的精度问题,两者支持的最大精度为6-7位和15-16位。
类型 | 指数长度 | 尾数长度 | 有效位 | 字节数 |
---|---|---|---|---|
float32 | 8 | 23 | 6-7 | 4 |
float64 | 11 | 52 | 15-16 | 8 |
FormatFloat
decimal
小数的数据结构如下:
type decimal struct {
d [800]byte // digits, big-endian representation
nd int // number of digits used
dp int // decimal point
neg bool // negative flag
trunc bool // discarded nonzero digits beyond d[:nd]
}
参数名 | 参数说明 |
---|---|
d | 存储小数的每一位数,此处的d存入的是计算后的数据,是包含数据整数及小数的近似值 |
nd | 小数的位数,末尾的0不计算在内 |
dp | 小数点的位置 |
neg | 是否负数 |
tranc | 是否丢弃nd之后的数据 |
数据转换
- 二进制数据转为uint64
获取二进制形式值对应的uint64,计算符号位、指数、尾数
func genericFtoa(dst []byte, val float64, fmt byte, prec, bitSize int) []byte {
var bits uint64
var flt *floatInfo
switch bitSize {
case 32:
bits = uint64(math.Float32bits(float32(val)))
flt = &float32info
case 64:
bits = math.Float64bits(val)
flt = &float64info
default:
panic("strconv: illegal AppendFloat/FormatFloat bitSize")
}
neg := bits>>(flt.expbits+flt.mantbits) != 0
exp := int(bits>>flt.mantbits) & (1<<flt.expbits - 1)
mant := bits & (uint64(1)<<flt.mantbits - 1)
...
if !ok {
return bigFtoa(dst, prec, fmt, neg, mant, exp, flt)
}
return formatDigits(dst, shortest, neg, digs, prec, fmt)
}
- 将指数、尾数部分转为[]byte
将尾数按位顺序存入
d.d
,然后根据指数右移,计算数据,更新d
func bigFtoa(dst []byte, prec int, fmt byte, neg bool, mant uint64, exp int, flt *floatInfo) []byte {
d := new(decimal)
d.Assign(mant)
d.Shift(exp - int(flt.mantbits))
...
}
// Assign v to a.
func (a *decimal) Assign(v uint64) {
var buf [24]byte
// Write reversed decimal in buf.
n := 0
for v > 0 {
v1 := v / 10
v -= 10 * v1
buf[n] = byte(v + '0')
n++
v = v1
}
// Reverse again to produce forward decimal in a.d.
a.nd = 0
for n--; n >= 0; n-- {
a.d[a.nd] = buf[n]
a.nd++
}
a.dp = a.nd
trim(a)
}
// Binary shift left (k > 0) or right (k < 0).
func (a *decimal) Shift(k int) {
switch {
case a.nd == 0:
// nothing to do: a == 0
case k > 0:
for k > maxShift {
leftShift(a, maxShift)
k -= maxShift
}
leftShift(a, uint(k))
case k < 0:
for k < -maxShift {
rightShift(a, maxShift)
k += maxShift
}
rightShift(a, uint(-k))
}
}
bigFtoa
从注释中可以看出,对于shortest
(prec<0
)的值取得是最小长度的精度,其他取得是round值。
// bigFtoa uses multiprecision computations to format a float.
func bigFtoa(dst []byte, prec int, fmt byte, neg bool, mant uint64, exp int, flt *floatInfo) []byte {
...
var digs decimalSlice
shortest := prec < 0
if shortest {
roundShortest(d, mant, exp, flt)
digs = decimalSlice{d: d.d[:], nd: d.nd, dp: d.dp}
// Precision for shortest representation mode.
switch fmt {
...
case 'f':
prec = max(digs.nd-digs.dp, 0)
...
}
} else {
// Round appropriately.
switch fmt {
...
case 'f':
d.Round(d.dp + prec)
...
}
digs = decimalSlice{d: d.d[:], nd: d.nd, dp: d.dp}
}
return formatDigits(dst, shortest, neg, digs, prec, fmt)
}
Round
舍入得规则如下:
先判断是否可以入,不能入则舍。
// Round a to nd digits (or fewer).
// If nd is zero, it means we're rounding
// just to the left of the digits, as in
// 0.09 -> 0.1.
func (a *decimal) Round(nd int) {
if nd < 0 || nd >= a.nd {
return
}
if shouldRoundUp(a, nd) {
a.RoundUp(nd)
} else {
a.RoundDown(nd)
}
}
具体的判断过程:
// If we chop a at nd digits, should we round up?
func shouldRoundUp(a *decimal, nd int) bool {
if nd < 0 || nd >= a.nd {
return false
}
if a.d[nd] == '5' && nd+1 == a.nd { // exactly halfway - round to even
// if we truncated, a little higher than what's recorded - always round up
if a.trunc {
return true
}
return nd > 0 && (a.d[nd-1]-'0')%2 != 0
}
// not halfway - digit tells all
return a.d[nd] >= '5'
}
- 如果需要小数长度
>=
当前小数的长度,即精度超出当前范围,直接舍。 - 如果需要的精度后的一位对应的数是5,且小数后只剩一位,则采用
偶数舍入
,向离的最近的偶数舍入
- 若丢弃后续的小数位,则一直
五入
。 - 精度>0,且小数精度对应的位为
奇数
,则五入
。 - 否则
五舍
。
- 若丢弃后续的小数位,则一直
- 否则,执行
四舍五入
。
结论
strconv.FormatFloat
在尾数舍入位为5
采用的是偶数舍入
(向离的最近的偶数舍入
),否则采用的是四舍五入
。
不过这是建立在float的尾数基础上的,单纯从小数形式难以直接看出结果。这个问题的根源仍在于有的小数无法用二进制精确的表示出来,只能用近似值表示,在处理中就需要注意精度问题。