Нельзя так просто взять и сделать учет денег. Всегда приходится думать, что же нам взять в качестве типов данных для хранения денег. Плюс, нам необходимы знания бухгалтерского учета.
Нужно понимать, что такое двойная запись, дебет, кредит, актив, пассив, план счетов и так далее. Есть статья, которая поможет разобраться в бух.учете для программистов.
Где проблемы при выборе типа данных?
Float в финансовых системах — это вечный поиск пропавшей копейки. Это происходит из-за плавающей точки. Подробнее о проблеме можно почитать тут.
Double/Decimal и прочие дробные числа нам подходят, если у нас есть поддержка со стороны БД (а она обычно есть) и со стороны языка.
Если же в стандартной библиотеке таких вещей нет, то стоит смотреть на целые числа. В случае с обычной финансовой системой мы можем хранить деньги в минимальных единицах (в центах, копейках) и форматировать их для фронтенда в зависимости от scale валюты, потому что даже с бумажными деньгами не во всех валютах одинаковое количество копеек. Например, в одном бахрейнском динаре 1000 флисов.
Поэтому нам удобнее хранить деньги в целых числах, когда у нас есть разные валюты с разным содержанием копеек. Мы можем сделать универсальный конвертер суммы в зависимости от количества точек после запятой.
А что нам делать, если нам нужны прям очень большие числа? Например, чтобы можно было поддерживать до 10^-10 после запятой? Тут даже обычных uint64 нам не хватит. А если нам еще их по сети надо передавать в разных форматах и по разным API?
Пришло время отличных историй
В Go для решения этой задачи есть math/big пакет с большими Int/Float/Rat числами. Все в них хорошо, кроме отсутствия перегруженной арифметики. Простейшие арифметические операции, например a+b, выглядит так: на каждую арифметическую операцию нужно создавать новое число через методы библиотеки:
Страшно становится тогда, когда нам надо сделать простую формулу, например:
v = price*.1-q*.1
Тогда наш код превращается во что-то страшное.
Помимо этого, есть сложность при работе с базой. Для этого нужно писать поддержку для database/sqlпакета.
Получился следующий код:
Как-то раз мы сидели, спокойно писали код на gRPC и gRPC-gateway, ничего не предвещало беды, пока не пришло время тестировать все то, что мы написали.
Протокол мы задизайнили таким образом, что все большие числа у нас передаются в бинарном формате, а в базе у нас используются строки.
В больших числах есть два метода:
SetString() - Парсит и выставляет значение для бинарного числа из строки. Например "3000000000000000000000"
SetBytes() - Парсит и выставляет значение для числа из набора байт
Во время тестирования мы столкнулись с тем, что в базе записывались слишком большие числа и не те, которые мы вводили. Причина банальна: кое-где были перепутаны большие числа в строковом значении, а где-то в бинарном формате.
При этом, нужно помнить, что в случае Go, gRPC, gRPC-gateway и БД у нас следующие типы данных:
- JSON — base64 в строке;
- gRPC — []byte;
- В базу нужно писать в строке.
Заключение
Баг мы, конечно, поправили.Он появился из-за невнимательности, и причина была в том, что 1000 в байтах не тоже самое, что 1000 в строке, потому что это разные типы.Правильный вариант выглядит примерно так.
Вот с такими сложностями мы столкнулись в Go при работе с большими числами. Если знаете больше, то поделитесь, пожалуйста, в комментариях.
Ранее статья была опубликована тут.