Как работать с деньгами в Go
Дробные типы в Go хранят значения в двоичном виде, и многие десятичные дроби в нём не представимы точно. Из-за этого простые суммы дают неожиданный результат, а ошибки копятся с каждой операцией. Ниже разберём, как считать деньги без потери копеек.
ℹ️ Почему
float64 не подходитКлассический пример:
fmt.Println(0.1 + 0.2) // 0.30000000000000004
На одной операции это незаметно, но в отчёте на тысячи позиций расхождение в копейки превращается в расхождение в рубли. Сравнивать такие суммы через
== тоже нельзя, потому что равные на бумаге значения в памяти отличаются.📎 Способ первый. Целые числа в минимальных единицах
Самый надёжный приём хранить сумму в наименьшей единице валюты, то есть в копейках или центах, как
int64. Целочисленная арифметика точна, сложение и вычитание не теряют ничего:
type Money int64 // сумма в копейках
func (m Money) String() string {
sign := ""
if m < 0 {
sign = "-"
m = -m
}
return fmt.Sprintf("%s%d.%02d", sign, m/100, m%100)
}
Цену 199 рублей 99 копеек вы храните как
Money(19999), а метод String собирает её обратно в читаемый вид. Этого подхода хватает для подавляющего большинства задач с фиксированной валютой.📎 Способ второй. Библиотека
shopspring/decimalЦелые числа неудобны там, где много делений и процентов, например при расчёте НДС или распределении суммы между участниками.
Тут помогает пакет
decimal, который хранит число как набор цифр с десятичной точкой и считает в десятичной системе:import "github.com/shopspring/decimal"
price := decimal.RequireFromString("199.99")
tax := price.Mul(decimal.RequireFromString("0.20"))
total := price.Add(tax).Round(2)
fmt.Println(total.StringFixed(2)) // 239.99
Обратите внимание, что значение задаётся строкой через
RequireFromString, а не из float64. Так в число не попадёт двоичная погрешность ещё на входе.📎 Округление и деление
При делении суммы остаток никуда не девается, и его нужно осознанно куда-то отнести.
decimal позволяет задать правило округления явно:total := decimal.RequireFromString("100.00")
share := total.Div(decimal.NewFromInt(3)).Round(2)
fmt.Println(share.StringFixed(2)) // 33.33Сумма трёх таких долей даст 99.99, и одну копейку придётся добавить к одной из частей вручную. Это не баг библиотеки, а свойство самих денег, и решать его нужно по правилам вашей предметной области.
Для денег в Go не используйте
float64 и точка.📍 Навигация: Вакансии • Задачи • Собесы • Канал в Max
🐸 Библиотека Go-разработчика
#GoDeep
