← Назад к вопросам

В каком формате удобно хранить деньги

2.3 Middle🔥 211 комментариев
#Основы Go#Производительность и оптимизация

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Формат хранения денежных значений в Go

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

Основные подходы

1. Целочисленные типы (в младших единицах)

Наиболее распространенный и рекомендуемый способ — хранение в минимальных единицах валюты (копейки, центы, сатоши) используя целочисленные типы:

// Хранение в копейках для рублей
type Money int64 // 1 рубль = 100 копеек

func NewMoney(rubles, kopecks int64) Money {
    return Money(rubles*100 + kopecks)
}

func (m Money) Rubles() int64 {
    return int64(m) / 100
}

func (m Money) Kopecks() int64 {
    return int64(m) % 100
}

func (m Money) String() string {
    return fmt.Sprintf("%d.%02d", m.Rubles(), m.Kopecks())
}

Преимущества:

  • Исключает ошибки округления при арифметических операциях
  • Высокая производительность (целочисленная арифметика)
  • Простота сравнения значений
  • Идеально для финансовых расчетов

Недостатки:

  • Требуется ручное преобразование при вводе/выводе
  • Нужно помнить о переполнении (использовать int64 или big.Int)

2. Тип decimal.Decimal (сторонние библиотеки)

Использование специализированных библиотек для десятичной арифметики:

import "github.com/shopspring/decimal"

// Создание денежного значения
price := decimal.NewFromFloat(123.45)

// Арифметические операции
total := price.Mul(decimal.NewFromInt(3))

// Форматирование
formatted := total.StringFixed(2) // "370.35"

Популярные библиотеки:

  • github.com/shopspring/decimal — наиболее популярная
  • github.com/ericlagergren/decimal — высокая производительность
  • github.com/cockroachdb/apd — от CockroachDB, для высокоточной арифметики

Преимущества:

  • Точные десятичные вычисления
  • Удобный API для финансовых операций
  • Поддержка разных режимов округления
  • Избегает проблем с двоичной арифметикой float

Недостатки:

  • Зависимость от сторонней библиотеки
  • Низкая производительность по сравнению с целыми числами
  • Больший расход памяти

3. Структуры с полями для основной и дробной частей

Более объектно-ориентированный подход:

type Money struct {
    Units    int64 // Основная единица (рубли, доллары)
    Nanos    int32 // Дробная часть (1 рубль = 10^9 nanos)
}

func NewMoney(units int64, nanos int32) Money {
    // Нормализация: nanos должен быть в диапазоне [0, 999999999]
    for nanos >= 1_000_000_000 {
        units++
        nanos -= 1_000_000_000
    }
    for nanos < 0 {
        units--
        nanos += 1_000_000_000
    }
    return Money{Units: units, Nanos: nanos}
}

Преимущества:

  • Четкое разделение целой и дробной частей
  • Легкое сериализация (например, для gRPC, Protobuf)
  • Понятная структура данных

Недостатки:

  • Сложнее арифметические операции
  • Больше кода для реализации

Критически важные рекомендации

Чего НЕ делать:

// НИКОГДА не используйте float32/float64 для денег!
var price float64 = 19.99
var quantity float64 = 3
total := price * quantity // 59.970000000000006 - ошибка!

Обязательные практики:

  1. Единообразие валюты — храните код валюты отдельно:
type Amount struct {
    Currency string // "USD", "RUB", "EUR"
    Value    int64  // в минимальных единицах
}
  1. Округление по банковским правилам (Round Half Even):
func RoundBankers(value int64, scale int) int64 {
    // Реализация банковского округления
}
  1. Проверка переполнения:
func Add(a, b int64) (int64, error) {
    if (b > 0 && a > math.MaxInt64 - b) || 
       (b < 0 && a < math.MinInt64 - b) {
        return 0, errors.New("overflow")
    }
    return a + b, nil
}

Производительность и точность

Для сравнения подходов:

  1. Целочисленный тип — оптимален для высоконагруженных систем, где важна производительность
  2. Decimal библиотеки — лучше для сложных финансовых расчетов с переменной точностью
  3. Структуры — удобны для систем с четкой сериализацией данных

Вывод

Для большинства финансовых приложений рекомендуется:

  • Использовать хранение в минимальных единицах валюты (копейки/центы) в int64
  • Для сложных расчетов с разными валютами и точностью — github.com/shopspring/decimal
  • Всегда сопровождать денежные значения кодом валюты
  • Реализовать строгую проверку переполнения и корректное округление

Пример комплексного решения:

package money

import (
    "errors"
    "fmt"
)

type Currency string

const (
    USD Currency = "USD"
    RUB Currency = "RUB"
    EUR Currency = "EUR"
)

type Money struct {
    currency Currency
    subunits int64 // центы/копейки
}

func New(currency Currency, units, subunits int64) Money {
    return Money{
        currency: currency,
        subunits: units*100 + subunits,
    }
}

func (m Money) Add(other Money) (Money, error) {
    if m.currency != other.currency {
        return Money{}, errors.New("currency mismatch")
    }
    
    // Проверка переполнения
    result := m.subunits + other.subunits
    if (other.subunits > 0 && result < m.subunits) || 
       (other.subunits < 0 && result > m.subunits) {
        return Money{}, errors.New("overflow")
    }
    
    return Money{
        currency: m.currency,
        subunits: result,
    }, nil
}

Такой подход обеспечивает безопасность, производительность и легкость поддержки финансовых операций в Go-приложениях.

В каком формате удобно хранить деньги | PrepBro