Стиль и правила 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.
| Ситуация | Правило |
|---|---|
| PATCH | Presence хранится отдельно, если *T не различает missing и null |
| Zero values | false, 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.
Версионируемые настройки сервиса — источник выполняемых проверок:
- CMS: mise.toml — форматирование, линтеры, тесты и генерация.
- Шаблон: AGENTS.md — правила запуска и владения исходниками.
- Скилл goravel-development — conventions, mocks и lifecycle тестового окружения.
При портировании дополнительно следуйте руководству и инструкциям конкретного сервиса.