Перейти к содержимому
Теория · Операторы · урок 7 из 9

Числа с плавающей точкой

Разделите на калькуляторе 1 на 3. На экране появится 0.3333333, хотя точный ответ — бесконечная дробь. Все тройки на экран не помещаются, и калькулятор хранит приближение.

С дробными числами в Go происходит то же самое. Go хранит числа типа float64 в двоичном виде, а в двоичной записи даже 0.1 — бесконечная дробь, как треть в десятичной. Поэтому сумма двух коротких дробей может оказаться не той, что получается на бумаге:

GO
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
}

Давайте разберём, что случилось, по строкам:

  1. a := 0.1 — вместо 0.1 в памяти лежит ближайшее к нему двоичное число.
  2. b := 0.2 — с 0.2 то же самое.
  3. fmt.Println(a + b) печатает 0.30000000000000004: две маленькие погрешности сложились и стали видны в последнем знаке.
  4. 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 возвращает модуль числа:

GO
a, b := 0.1, 0.2
fmt.Println(math.Abs(a+b-0.3) < 1e-9) // true

Разберём выражение по частям:

  1. a+b-0.3 — разница между суммой и ожидаемым ответом, около 0.00000000000000006.
  2. math.Abs(...) убирает знак: неважно, в какую сторону ошиблась сумма.
  3. < 1e-9 сравнивает с допуском. Запись 1e-9 значит 0.000000001, то есть единица, умноженная на 10 в степени −9.

Допуск выбирают по задаче. Он должен быть больше погрешности, но меньше разницы, которая важна.

Задачи курса проверяют дробные ответы с допуском 1e-9. Слишком маленький допуск, например 1e-20, для чисел порядка единицы меньше любой погрешности и снова превращает сравнение в ==.

Функции пакета math

В том же пакете есть math.Sqrt — квадратный корень. Обе функции принимают и возвращают float64:

GO
fmt.Println(math.Sqrt(2))   // 1.4142135623730951
fmt.Println(math.Sqrt(16))  // 4
fmt.Println(math.Abs(-2.5)) // 2.5
  • math.Sqrt(2) печатает приближение корня из двух: точное значение — бесконечная дробь.
  • math.Sqrt(16) печатает 4: fmt.Println печатает целый результат вроде 4.0 без точки, хотя тип остаётся float64.
  • math.Abs(-2.5) печатает 2.5.

Частая ошибка

В math.Sqrt передают переменную типа int:

GO
n := 16
fmt.Println(math.Sqrt(n)) // ✗
TEXT
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 и подпишись на канал — откроются все уроки.

Войти другим способом