Найти в Дзене
Mad Devs

Боль и нищета больших чисел в Go

Оглавление

Нельзя так просто взять и сделать учет денег. Всегда приходится думать, что же нам взять в качестве типов данных для хранения денег. Плюс, нам необходимы знания бухгалтерского учета.

Нужно понимать, что такое двойная запись, дебет, кредит, актив, пассив, план счетов и так далее. Есть статья, которая поможет разобраться в бух.учете для программистов.

Где проблемы при выборе типа данных?

Float в финансовых системах — это вечный поиск пропавшей копейки. Это происходит из-за плавающей точки. Подробнее о проблеме можно почитать тут.

Double/Decimal и прочие дробные числа нам подходят, если у нас есть поддержка со стороны БД (а она обычно есть) и со стороны языка.

Если же в стандартной библиотеке таких вещей нет, то стоит смотреть на целые числа. В случае с обычной финансовой системой мы можем хранить деньги в минимальных единицах (в центах, копейках) и форматировать их для фронтенда в зависимости от scale валюты, потому что даже с бумажными деньгами не во всех валютах одинаковое количество копеек. Например, в одном бахрейнском динаре 1000 флисов.

Поэтому нам удобнее хранить деньги в целых числах, когда у нас есть разные валюты с разным содержанием копеек. Мы можем сделать универсальный конвертер суммы в зависимости от количества точек после запятой.

А что нам делать, если нам нужны прям очень большие числа? Например, чтобы можно было поддерживать до 10^-10 после запятой? Тут даже обычных uint64 нам не хватит. А если нам еще их по сети надо передавать в разных форматах и по разным API?

Пришло время отличных историй

В Go для решения этой задачи есть math/big пакет с большими Int/Float/Rat числами. Все в них хорошо, кроме отсутствия перегруженной арифметики. Простейшие арифметические операции, например a+b, выглядит так: на каждую арифметическую операцию нужно создавать новое число через методы библиотеки:

-2

Страшно становится тогда, когда нам надо сделать простую формулу, например:

v = price*.1-q*.1

Тогда наш код превращается во что-то страшное.

Помимо этого, есть сложность при работе с базой. Для этого нужно писать поддержку для database/sqlпакета.

Получился следующий код:

Как-то раз мы сидели, спокойно писали код на gRPC и gRPC-gateway, ничего не предвещало беды, пока не пришло время тестировать все то, что мы написали.

Протокол мы задизайнили таким образом, что все большие числа у нас передаются в бинарном формате, а в базе у нас используются строки.

В больших числах есть два метода:

SetString() - Парсит и выставляет значение для бинарного числа из строки. Например "3000000000000000000000"
SetBytes() - Парсит и выставляет значение для числа из набора байт

Во время тестирования мы столкнулись с тем, что в базе записывались слишком большие числа и не те, которые мы вводили. Причина банальна: кое-где были перепутаны большие числа в строковом значении, а где-то в бинарном формате.

При этом, нужно помнить, что в случае Go, gRPC, gRPC-gateway и БД у нас следующие типы данных:

  • JSON — base64 в строке;
  • gRPC — []byte;
  • В базу нужно писать в строке.

Заключение

Баг мы, конечно, поправили.Он появился из-за невнимательности, и причина была в том, что 1000 в байтах не тоже самое, что 1000 в строке, потому что это разные типы.Правильный вариант выглядит примерно так.

Вот с такими сложностями мы столкнулись в Go при работе с большими числами. Если знаете больше, то поделитесь, пожалуйста, в комментариях.

Ранее статья была опубликована тут.