В чем сложность работы с float в Go
Что проверяют
Знаете ли вы, что float64 в Go это обычный IEEE 754 со всеми его свойствами, и какие особенности добавляет сам язык. Следом обычно спрашивают, как хранить деньги и почему 0.1 + 0.2 == 0.3 в Go иногда даёт true.
Как отвечать
- двоичное представление:
0.1в двоичной записи бесконечная дробь, хранится ближайшее значение. Уfloat64около 15 надёжных десятичных знаков, уfloat32около 7, ошибки копятся при сложении чисел разного масштаба; - константы без типа: Go вычисляет их точно, как рациональные числа. Поэтому
0.1 + 0.2 == 0.3на константах даётtrue, а те же значения в переменныхfalse; - сравнение: вместо
==сравнивают с допуском, лучше относительным:math.Abs(a-b) <= eps*math.Max(math.Abs(a), math.Abs(b)). Фиксированный1e-10бессмыслен для чисел порядка миллиарда; - особые значения: деление переменной на ноль даёт
+Infбез паники (целочисленное паникует),NaNне равен даже себе, проверка черезmath.IsNaN; - NaN в ключе мапы: каждая запись создаёт новый элемент, найти его нельзя, удалить можно только через
clear(Go 1.21+); - FMA: спецификация разрешает сливать умножение и сложение в одну операцию, поэтому на arm64 результат может отличаться от amd64 в последнем бите. Явное
float64(x*y)слияние запрещает; - преобразование в int: дробная часть отбрасывается, а при выходе за диапазон паники нет, результат зависит от платформы.
Деньги хранят в целых копейках (int64) или в десятичном типе вроде shopspring/decimal.
Где спотыкаются
Советуют math/big для денег. big.Float тоже двоичный и 0.1 точно не хранит, точен только big.Rat, но он медленный.
Демонстрируют ошибку примером a := 0.1 + 0.2, а это константное выражение, и сравнение с 0.3 проходит.
Считают, что эпсилон решает всё. Сравнение с допуском нетранзитивно: из a≈b и b≈c не следует a≈c, поэтому для сортировки и группировки оно ломает логику, а для денег неприемлемо вовсе.