From the public account: Gopher
Money is an ancient mystery. If it has it, it will be strong and strong, but if it lacks it will be humble and weak.
In a system, especially a system related to money, money is the most important thing, and the accuracy of calculation will be the topic of this article.
Why Precision Matters
"Plume and sinking boat" is the most suitable for use here. If an e-commerce platform has an annual order turnover of 1 billion, and each order is settled by 1 cent less, the cumulative loss 10 million! In a word, this lost money is one-tenth of Wang's small goal. If due to accuracy problems, when paying customers for settlement, undercounting will lose customers, and overcounting will lose money. This shows that accurate calculation of money is very important!
Why is there a problem with accuracy
The classic case, let's see if 0.3
0.1 + 0.2
the computer.
Those who have studied computers in the above cases should know that computers are binary. When using binary to represent floating-point numbers (IEEE754 standard), only a small number of numbers can be accurately represented by this method. Let's take 0.3 as an example to see the process of converting from decimal to binary.
Computers are limited in the number of digits, so computers can't get accurate results when they calculate with floating-point numbers. This hard limit cannot be broken, so precision needs to be introduced to ensure that the calculation of money is as accurate as possible within the allowable error range.
The actual representation of floating-point numbers in computers will not be discussed further in this article. You can refer to the following connections for learning:
Single-precision floating-point representation:
https://en.wikipedia.org/wiki/Single-precision_floating-point_format
Double-precision floating-point representation:
https://en.wikipedia.org/wiki/Double-precision_floating-point_format
Floating point converter:
Calculating with floating point numbers
Taking the above 0.1 + 0.2
as an example, the error of 0.00000000000000004
can be completely ignored. We try to retain 5 digits of precision for the decimal part, and see the results below.
The results at this point are as expected. This is why the form of a - b <= 0.00001
is often used to judge whether two floating-point numbers are equal. To put it bluntly, this is another form of representation that retains 5-digit precision in the fractional part.
Compute with integers
As mentioned earlier, only a small number of floating-point numbers can be represented by the IEEE754 standard, while integers can accurately represent numbers in all valid ranges. So it's easy to think of using integers to represent floating-point numbers.
For example, if the decimal is pre-determined to retain 8-bit precision, then 0.1
and 0.2
are represented as integers as 10000000
and 20000000
respectively, and the operation of floating-point numbers is also converted to the operation of integer. Take 0.1 + 0.2
as an example.
// 表示小数位保留8位精度
const prec = 100000000
func float2Int(f float64) int64 {
return int64(f * prec)
}
func int2float(i int64) float64 {
return float64(i) / prec
}
func main() {
var a, b float64 = 0.1, 0.2
f := float2Int(a) + float2Int(b)
fmt.Println(a+b, f, int2float(f))
return
}
The output of the above code is as follows:
The output above is exactly as expected, so using integers to represent floating-point numbers seems like a viable solution. However, we cannot limit ourselves to a single case, and more tests are needed.
fmt.Println(float2Int(2.3))
The output of the above code is as follows:
This result is so unexpected, but it is reasonable.
The above figure represents the actual storage value of 2.3
in the computer, so the result of conversion using the float2Int
function is 229999999
instead of 230000000
.
This result is obviously not in line with expectations, and there is still a loss of accuracy within the determined accuracy range. If this code is sent online, there is a high probability that you will leave at the speed of light the next day. To solve this problem is also very simple, just introduce github.com/shopspring/decimal
, see the corrected code below.
// 表示小数位保留8位精度
const prec = 100000000
var decimalPrec = decimal.NewFromFloat(prec)
func float2Int(f float64) int64 {
return decimal.NewFromFloat(f).Mul(decimalPrec).IntPart()
}
func main() {
fmt.Println(float2Int(2.3)) // 输出:230000000
}
At this time, the result is as expected, and the floating-point operations (addition, subtraction, and multiplication) in the system can be converted into integer operations, and the operation result only needs one floating-point conversion.
So far, the use of integer computing can basically meet most scenarios, but there are still two problems that need to be paid attention to.
1. Integer indicates whether the range of floating-point numbers meets the system requirements.
2. When integers represent floating-point numbers, division still needs to be converted to floating-point number operations.
integer represents the range of floating point numbers
Taking int64
as an example, the value range is -9223372036854775808~9223372036854775807
. If we reserve 8 digits for the precision of the decimal part, the remaining integer part still has 11 digits, that is, if it only represents money, it can still store an amount of tens of billions. This value is suitable for many systems. It is more than enough for small and medium-sized companies, but the range is still an issue that needs careful consideration when using this method to store amounts.
integer represents the division of floating point numbers
There is no implicit conversion of integer to floating point in Go, that is, the result of dividing an integer and an integer is still an integer. When we represent floating-point numbers as integers, we need to pay special attention to the fact that the division of integers will lose all fractional parts, so we must convert to floating-point numbers before dividing.
Maximum precision for floating point and integer types
The range of int64
is -9223372036854775808~9223372036854775807
, then when the floating-point type is represented by an integer type, the effective decimal digits of the integer part and the fractional part are at most 19
digits.
The range of uint64
is 0~18446744073709551615
, then when using integer to represent floating point, the valid decimal digits of integer part and fractional part are at most 20
digits, because the system generally does not store negative numbers when representing the amount, so compared with int64
, it is more recommended Use uint64
.
float64
based on the IEEE754 standard, and with reference to Wikipedia, it is known that the significant decimal digits of its integer part and fractional part are 15-17
bits.
Let's look at the example below.
var (
a float64 = 123456789012345.678
b float64 = 1.23456789012345678
)
fmt.Println(a, b, decimal.NewFromFloat(a), a == 123456789012345.67)
return
The output of the above code is as follows:
According to the output result, float64
cannot represent a decimal number with more than 17 significant digits. In terms of effective decimal digits, Lao Xu recommends using integers to represent floating-point numbers.
Try to retain as much precision as possible in the calculation
The importance of precision and the maximum precision that can be represented by integers and floating-point types have been mentioned earlier. Let's take a practical example to discuss whether to retain the specified precision during the calculation process.
var (
// 广告平台总共收入7.11美元
fee float64 = 7.1100
// 以下是不同渠道带来的点击数
clkDetails = []int64{220, 127, 172, 1, 17, 1039, 1596, 200, 236, 151, 91, 87, 378, 289, 2, 14, 4, 439, 1, 2373, 90}
totalClk int64
)
// 计算所有渠道带来的总点击数
for _, c := range clkDetails {
totalClk += c
}
var (
floatTotal float64
// 以浮点数计算每次点击的收益
floatCPC float64 = fee / float64(totalClk)
intTotal int64
// 以8位精度的整形计算每次点击的收益(每次点击收益转为整形)
intCPC int64 = float2Int(fee / float64(totalClk))
intFloatTotal float64
// 以8位进度的整形计算每次点击的收益(每次点击收益保留为浮点型)
intFloatCPC float64 = float64(float2Int(fee)) / float64(totalClk)
decimalTotal = decimal.Zero
// 以decimal计算每次点击收益
decimalCPC = decimal.NewFromFloat(fee).Div(decimal.NewFromInt(totalClk))
)
// 计算各渠道点击收益,并累加
for _, c := range clkDetails {
floatTotal += floatCPC * float64(c)
intTotal += intCPC * c
intFloatTotal += intFloatCPC * float64(c)
decimalTotal = decimalTotal.Add(decimalCPC.Mul(decimal.NewFromInt(c)))
}
// 累加结果对比
fmt.Println(floatTotal) // 7.11
fmt.Println(intTotal) // 710992893
fmt.Println(decimal.NewFromFloat(intFloatTotal).IntPart()) // 711000000
fmt.Println(decimalTotal.InexactFloat64()) // 7.1100000000002375
Compared with the above calculation results, only the second type has the lowest precision, and the main reason for the loss of this precision is that float2Int(fee / float64(totalClk))
only retains the precision of the intermediate calculation result by 8
bits, so there is an error in the result. Other calculation methods retain as much precision as possible in the intermediate calculation process so the results are as expected.
Combination of division and subtraction
According to the previous description, use floating-point numbers and retain as much precision as possible in the calculation of division. This still does not solve all problems, let's look at the following example.
// 1元钱分给3个人,每个人分多少?
var m float64 = float64(1) / 3
fmt.Println(m, m+m+m)
The output of the above code is as follows:
From the calculation results, each person 0.3333333333333333
yuan, and when the money shared by each person is aggregated again, it becomes 1
yuan, then
This 0.0000000000000001
yuan jumped out of the stone! Sometimes I really don't understand these computers.
This result is obviously not in line with human intuition. In order to be more in line with intuition, we combine subtraction to complete this calculation.
// 1元钱分给3个人,每个人分多少?
var m float64 = float64(1) / 3
fmt.Println(m, m+m+m)
// 最后一人分得的钱使用减法
m3 := 1 - m - m
fmt.Println(m3, m+m+m3)
The output of the above code is as follows:
Through subtraction we finally recovered the lost 0.0000000000000001
yuan. Of course, the above is just an example given by Xu. In the actual calculation process, it may be necessary to perform subtraction through the decimal
library to ensure that the money does not disappear or increase out of thin air.
The above are all superficial opinions of Lao Xu. If you have any doubts and mistakes, please point out in time. I sincerely hope that this article can be helpful to all readers.
Note:
At the time of writing this article, the go version used by the author is: go1.16.6
Some examples used in the article: https://github.com/Isites/go-coder/blob/master/money/main.go
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。