Числа с плавающей точкой
Разделите на калькуляторе 1 на 3. На экране появится 0.3333333, хотя точный ответ — бесконечная дробь. Все тройки на экран не помещаются, и калькулятор хранит приближение.
С дробными числами в Go происходит то же самое. Go хранит числа типа float64 в двоичном виде, а в двоичной записи даже 0.1 — бесконечная дробь, как треть в десятичной. Поэтому сумма двух коротких дробей может оказаться не той, что получается на бумаге:
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
}Давайте разберём, что случилось, по строкам:
a := 0.1— вместо0.1в памяти лежит ближайшее к нему двоичное число.b := 0.2— с0.2то же самое.fmt.Println(a + b)печатает0.30000000000000004: две маленькие погрешности сложились и стали видны в последнем знаке.fmt.Println(a+b == 0.3)печатаетfalse: сумма не совпала с0.3точно.
Здесь аналогия с калькулятором неполная. Калькулятор показывает, что округлил, а Go при печати выбирает самую короткую запись: fmt.Println(a) выведет 0.1, хотя в памяти лежит приближение. Погрешность становится видна только после вычислений.
Это классический вопрос на собеседовании: почему 0.1 + 0.2 не равно 0.3? Ответ: десятичные дроби хранятся в двоичном виде приближённо, и погрешности складываются.
Как хранится дробь
Такие числа называют числами с плавающей точкой. Их устройство сводится к трём фактам:
- число хранит около 16 значащих цифр;
- положение точки задаёт отдельный множитель, поэтому
float64вмещает и0.000001, и миллиарды; - любое число в нём приближённое.
Поэтому деньги в float64 не хранят: копейки считают целым числом типа int.
Два дробных типа
В Go два типа для дробных чисел. Они отличаются размером и точностью.
float64
Занимает 8 байт и хранит около 16 значащих цифр. Дробный литерал вроде 0.1 по умолчанию получает именно этот тип, и функции пакета math работают с ним.
Для обычных расчётов берите float64.
float32
Занимает 4 байта и хранит примерно 7 значащих цифр. Треть в нём выглядит как 0.33333334: восьмая цифра уже неточная.
Сложить float32 и float64 без преобразования нельзя: как и у целых, типы операндов должны совпадать.
Константы считаются точно
Константы-литералы Go складывает точно, ещё до запуска программы. Поэтому fmt.Println(0.1 + 0.2) напечатает 0.3, а погрешность появляется в переменных.
Сравнение с допуском
Дробные результаты не сравнивают через ==. Вместо этого проверяют, что разница меньше маленького допуска. Функция math.Abs из пакета math возвращает модуль числа:
a, b := 0.1, 0.2
fmt.Println(math.Abs(a+b-0.3) < 1e-9) // trueРазберём выражение по частям:
a+b-0.3— разница между суммой и ожидаемым ответом, около0.00000000000000006.math.Abs(...)убирает знак: неважно, в какую сторону ошиблась сумма.< 1e-9сравнивает с допуском. Запись1e-9значит0.000000001, то есть единица, умноженная на 10 в степени −9.
Допуск выбирают по задаче. Он должен быть больше погрешности, но меньше разницы, которая важна.
Задачи курса проверяют дробные ответы с допуском 1e-9. Слишком маленький допуск, например 1e-20, для чисел порядка единицы меньше любой погрешности и снова превращает сравнение в ==.
Функции пакета math
В том же пакете есть math.Sqrt — квадратный корень. Обе функции принимают и возвращают float64:
fmt.Println(math.Sqrt(2)) // 1.4142135623730951
fmt.Println(math.Sqrt(16)) // 4
fmt.Println(math.Abs(-2.5)) // 2.5math.Sqrt(2)печатает приближение корня из двух: точное значение — бесконечная дробь.math.Sqrt(16)печатает4:fmt.Printlnпечатает целый результат вроде4.0без точки, хотя тип остаётсяfloat64.math.Abs(-2.5)печатает2.5.
Частая ошибка
В math.Sqrt передают переменную типа int:
n := 16
fmt.Println(math.Sqrt(n)) // ✗cannot use n (variable of type int) as float64 value in argument to math.SqrtПрограмма не компилируется. Литерал 16 подстраивается под нужный тип, а переменная n уже имеет тип int, и Go не преобразует её сам. Преобразуйте аргумент явно: math.Sqrt(float64(n)) даёт 4.
Главное
float64хранит дроби приближённо, поэтому в переменных0.1 + 0.2не равно0.3.- Дробные результаты сравнивают с допуском:
math.Abs(a+b-0.3) < 1e-9. - Деньги не хранят в
float64: копейки считают целым числом. math.Sqrtиmath.Absпринимаютfloat64, а переменную типаintпреобразуют явно.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.