Перейти к основному содержимому

Стиль и правила Go-кода

Новый код следует структуре и инструментам своего сервиса. Версии Go и зависимостей определяются go.mod, инструменты — mise.toml и mise.lock, локальные правила — AGENTS.md. Пример из другого сервиса нужно сверить с этими источниками, а не переносить вместе с его историческими особенностями.

Форматирование и имена​

Из корня сервиса:

elc mise run format
elc mise run check:fmt

После форматирования просмотрите diff: команда обрабатывает Go-файлы репозитория. check:fmt проверяет без перезаписи, check:fmt-staged проверяет именно staged-версию. Если исправили файл после git add, обновите staged-содержимое перед commit.

Для ручного Go-кода:

  • пакеты называются короткими строчными именами; файлы — в принятом рядом snake_case;
  • экспортируемые типы и методы имеют понятные имена: PatchOrderAction, Execute, OrderID, HTTPClient;
  • локальные имена отражают смысл, короткие receiver/loop names уместны в небольшом scope;
  • используется any, если интерфейсу не нужны методы;
  • комментарий объясняет причину или ограничение, а экспортируемый helper документирует контракт.

Generated DTO могут содержать Id вместо ID и другие особенности генератора. Не переименовывайте их вручную ради единообразия; преобразуйте имена на границе mapper-а. Избегайте массовых переименований и форматирования соседнего кода вне задачи.

Границы слоёв и зависимостей​

ГдеОтветственность
handlersВвод, validation, вызов action, HTTP-ответ
actionsСамостоятельный сценарий с Execute(context.Context, ...), бизнес-правила и координация эффектов
queriesРазрешённые filters/sorts/includes для поиска
mappersЯвное преобразование модели в контрактный DTO
adaptersИнфраструктурные модели и интеграции
commonПроверенные общие механизмы без зависимости от HTTP-слоя конкретного модуля

Не передавайте http.Context глубоко в action ради доступа к request: выделите типизированные параметры и обычный context.Context. Доменная ошибка возвращается как error; HTTP-статус выбирает handler через общие helpers.

Модели и их фабрики находятся в app/adapters/db, бизнес-код — в соответствующем модуле. Новую абстракцию выделяйте при понятной ответственности и потребителе. Не создавайте второй HTTP-клиент, search engine или параллельный каталог models, когда подходящий механизм уже есть в сервисе.

Для facades используйте принятый в репозитории пакет и способ разрешения binding. Singleton может хранить неизменяемую конфигурацию и безопасный для совместного использования клиент. Данные текущего запроса в нём хранить нельзя.

Подробности и примеры — в структуре сервиса, поиске и HTTP-клиентах.

Типы, presence и преобразования​

Явно различайте транспортный DTO, доменное значение и ORM-модель. gorm tags описывают SQL, json tags — JSON; они не заменяют друг друга. Для публичного ответа предпочтителен mapper с явными полями, особенно при датах, nullable-значениях и условных include.

СитуацияПравило
PATCHPresence хранится отдельно, если *T не различает missing и null
Zero valuesfalse, 0, "" и пустой slice проверяются по контракту; не теряются при ORM update или omitempty
NullableУказатель или иной согласованный nullable-тип; nil и пустое значение имеют разный смысл
ИдентификаторТип и диапазон сверяются с контрактом и схемой; преобразование не должно переполняться или менять смысл FK
Деньги и количестваЕдиницы и точность задаёт домен; произвольный переход через float64 недопустим
ВремяСогласованные timezone, точность БД и сериализация DTO

Не связывайте политику отсутствующих данных с zero value автоматически: не найденная модель может требовать 404, пустая коллекция — [], а nullable-отношение — null. Преобразование через JSON с omitempty способно скрыть false и 0; проверьте реализацию generic mapper перед его применением.

Вложенные maps/slices и указатели остаются общими после поверхностного копирования struct. Для before/after и dirty comparison сохраняйте снимок, который не изменится при обновлении оригинала. Примеры presence и no-op — в руководстве по изменениям.

Ошибки и освобождение ресурсов​

Обрабатывайте ошибку рядом с операцией. Возвращайте cause либо добавляйте полезный контекст с %w:

if err != nil {
return fmt.Errorf("load order %d: %w", orderID, err)
}

Для проверки причины используйте errors.Is/errors.As и принятые типы из app/errors. Сравнение строк ошибки ломается при обёртке. Не заменяйте сбой пустым успешным результатом и не логируйте его на каждом слое: следуйте общей точке логирования HTTP/consumer/command.

_ = operation() допустимо только при явно принятой политике игнорирования результата. Особенно внимательно проверяйте cleanup, commit, RowsAffected, запись файла и отправку сообщения. Best-effort эффект должен иметь понятное поведение после отказа.

Для ресурса с обязательным Close определите владельца и обработку обеих ошибок. Пример helper-а, который принимает владение непустым stream:

func readAndClose(body io.ReadCloser) (data []byte, err error) {
defer func() {
if closeErr := body.Close(); closeErr != nil {
err = errors.Join(err, fmt.Errorf("close body: %w", closeErr))
}
}()
return io.ReadAll(body)
}

Здесь используются стандартные io, errors, fmt. Если generated HTTP-клиент уже разобрал и закрыл response, повторно управлять stream не нужно. Не закрывайте ресурс, жизненным циклом которого владеет вызывающий framework.

Публичные error code/status и допустимые тексты определяются API-контрактом. Правила преобразования ошибок описаны в HTTP-ответах.

Контекст и конкурентное выполнение​

Передавайте входящий context в ORM, исходящие HTTP-вызовы и storage. Не заменяйте его на context.Background() внутри сценария: это теряет отмену, deadline и контекст инициирующего события. Отдельный жизненный цикл фоновой работы задаётся worker-ом или командой.

Не запускайте goroutine из handler/action без определённого владельца, ожидания результата, обработки ошибки и завершения. Для фонового выполнения используйте существующие механизмы очередей и Kafka, если они соответствуют сценарию. При изменении конкурентного кода дополнительно запускают elc mise run check:race.

Maps, mutable clients, fake registry и globals требуют отдельной оценки конкурентного доступа. Context не делает хранящиеся в нём объекты автоматически безопасными.

SQL и внешние эффекты​

Внутри SQL-транзакции все связанные записи используют переданный orm.Query. Вызов facades.Orm().Query() из вложенного действия создаёт другую границу выполнения. Зафиксируйте, кто начинает и завершает транзакцию.

SQL rollback не отменяет отправленный HTTP-запрос, Kafka-сообщение или удалённый файл. Порядок commit и эффектов следует архитектуре сценария, а не удобству расположения кода. Отказ обязательного эффекта нельзя превращать в успех одним логом. Подробнее — изменяющие операции и файлы.

Проверяемость и тесты​

Для каждого изменения выберите наблюдаемое поведение: ответ API, сохранённые значения после повторного чтения SQL, точные внешние вызовы или содержимое storage. Тест, повторяющий присваивания реализации без проверки результата, мало что подтверждает.

  • Используйте существующие suite, фабрики и TestMain; общая изоляция описана в автотестах.
  • Для параллельных по смыслу случаев удобны table-driven тесты с t.Run; lifecycle suite при ручном запуске нужно вызывать явно.
  • В обычном тесте используйте assert.*(t, ...)/require.*(t, ...), в suite — s.*/s.Require().
  • После ошибки запроса или декодирования применяйте require до доступа к response, указателю и элементу коллекции.
  • Для mock задавайте .Once()/.Times(n) и конкретные аргументы; .Maybe() используйте при обоснованно необязательном вызове.
  • Для sort/page утверждайте порядок ID, для отрицательной ветки — также отсутствие SQL-изменений и эффектов.
  • Тесты с общей PostgreSQL-БД, singleton bindings или fake registry выполняйте последовательно.

Начинайте с затронутого пакета, затем выполняйте проверки, предусмотренные сервисом. Например, для CMS pages:

elc go test ./app/modules/contents/tests/pages --env=.env.testing -count=1
elc mise run check:fmt
elc mise run check:vet
elc go build -o build/app

Для тестов всегда передаётся .env.testing, включая узкий запуск. Полный список проверок шаблона приведён в первом запуске.

Generated-код и источники правил​

Файлы с Code generated/DO NOT EDIT меняются через источник и генерацию. У файлов, которые генератор сохраняет для ручной доработки, сверяйте фактическую политику обновления — см. ensi-gog. Миграции находятся в database/migrations и регистрируются в bootstrap/migrations.go.

Версионируемые настройки сервиса — источник выполняемых проверок:

При портировании дополнительно следуйте руководству и инструкциям конкретного сервиса.