Floating-point numbers
Divide 1 by 3 on a calculator. The screen shows 0.3333333, even though the exact answer is an endless fraction. Not all the threes fit on the screen, so the calculator keeps an approximation.
The same happens to fractional numbers in Go. Numbers of type float64 are stored in binary, and in binary even 0.1 is an endless fraction, like a third in decimal. So the sum of two short fractions may differ from the one on paper:
package main
import "fmt"
func main() {
a := 0.1
b := 0.2
fmt.Println(a + b) // 0.30000000000000004
fmt.Println(a+b == 0.3) // false
}Let's look at what happened, line by line:
a := 0.1: memory holds the binary number closest to0.1instead of0.1itself.b := 0.2: the same goes for0.2.fmt.Println(a + b)prints0.30000000000000004: two tiny rounding errors added up and showed in the last digit.fmt.Println(a+b == 0.3)printsfalse: the sum did not match0.3exactly.
Here the calculator picture falls short. A calculator shows that it rounded, while Go prints the shortest form: fmt.Println(a) prints 0.1, even though memory holds an approximation. The error becomes visible only after a calculation.
This is a classic interview question: why is 0.1 + 0.2 not equal to 0.3? The answer: decimal fractions are stored in binary approximately, and the errors add up.
How a fraction is stored
Such numbers are called floating-point numbers. The way they work comes down to three facts:
- a number keeps about 16 significant digits;
- a separate factor sets where the point goes, so a
float64holds both0.000001and billions; - every number in it is approximate.
That is why money is not kept in a float64: cents are counted as an integer of type int.
Two fractional types
Go has two types for fractional numbers. They differ in size and precision.
float64
It takes 8 bytes and keeps about 16 significant digits. A fractional literal such as 0.1 gets this type by default, and the functions of the math package work with it.
For ordinary calculations, use float64.
float32
It takes 4 bytes and keeps about 7 significant digits. A third in it looks like 0.33333334: the eighth digit is already off.
You cannot add a float32 and a float64 without a conversion: as with integers, the operand types must match.
Constants are exact
Go adds constant literals exactly, before the program runs. So fmt.Println(0.1 + 0.2) prints 0.3, and the error appears in variables.
Comparing with a tolerance
Fractional results are not compared with ==. Instead, the check is that the difference is below a small tolerance. The math.Abs function from the math package returns the absolute value of a number:
a, b := 0.1, 0.2
fmt.Println(math.Abs(a+b-0.3) < 1e-9) // trueLet's take the expression apart:
a+b-0.3is the difference between the sum and the expected answer, about0.00000000000000006.math.Abs(...)drops the sign: it does not matter which way the sum was off.< 1e-9compares with the tolerance. The notation1e-9means0.000000001, that is, one times 10 to the power of −9.
The tolerance depends on the task. It must be larger than the rounding error but smaller than any difference that matters.
The course exercises check fractional answers with a tolerance of 1e-9. A tolerance that is too small, such as 1e-20, is below any rounding error for numbers around one and turns the comparison back into ==.
Functions of the math package
The same package has math.Sqrt, the square root. Both functions take and return a float64:
fmt.Println(math.Sqrt(2)) // 1.4142135623730951
fmt.Println(math.Sqrt(16)) // 4
fmt.Println(math.Abs(-2.5)) // 2.5math.Sqrt(2)prints an approximation of the square root of two: the exact value is an endless fraction.math.Sqrt(16)prints4:fmt.Printlnprints a whole result such as4.0without a point, even though the type staysfloat64.math.Abs(-2.5)prints2.5.
Common mistake
A variable of type int is passed to math.Sqrt:
n := 16
fmt.Println(math.Sqrt(n)) // ✗cannot use n (variable of type int) as float64 value in argument to math.SqrtThe program does not compile. The literal 16 adapts to the required type, but the variable n already has type int, and Go never converts it implicitly. Convert the argument explicitly: math.Sqrt(float64(n)) gives 4.
Key points
- A
float64stores fractions approximately, so in variables0.1 + 0.2is not equal to0.3. - Fractional results are compared with a tolerance:
math.Abs(a+b-0.3) < 1e-9. - Money is not kept in a
float64: cents are counted as an integer. math.Sqrtandmath.Abstake afloat64, so a variable of typeintis converted explicitly.
Subscribe to take the course
The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.