From 76af14cb964225a8ff2ccf71c1a51571cb7b88d6 Mon Sep 17 00:00:00 2001 From: Fedorov Vladimir Date: Thu, 1 Oct 2026 13:03:14 +0700 Subject: [PATCH] add standart --- AGENTS.md | 24 ++-- docs/GO_STANDARD.md | 275 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 282 insertions(+), 17 deletions(-) create mode 100644 docs/GO_STANDARD.md diff --git a/AGENTS.md b/AGENTS.md index 6314c5f..a297994 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,26 +9,16 @@ - S3-совместимое хранилище для файлов; - MCP встроен в основной сервис. -## Структура +## Стандарт Go-кода -- `api/` — protobuf-контракт; -- `proto/` — сгенерированный Go-код; -- `internal/app/` — gRPC/HTTP handlers; -- `internal/services/` — бизнес-логика; -- `internal/repos/` — работа с PostgreSQL; -- `internal/modules/` — переиспользуемые модули; -- `migrations/` — миграции базы данных. +[`docs/GO_STANDARD.md`](docs/GO_STANDARD.md) — единственный источник правил +написания Go-кода. Для задач, затрагивающих Go-бэкенд, соблюдай его требования. -## Правила +## Проектные ограничения -1. Сначала найди всех callers изменяемой функции и существующий паттерн. -2. Делай минимальное изменение; каждый модуль разделяй на интерфейс и реализацию. -3. Бизнес-логику держи в services, SQL — в repos, транспорт — в app. -4. Новые HTTP-методы добавляй через `api/main.proto` и регенерируй код. -5. `proto/` и Swagger — сгенерированные файлы; руками их не редактируй. -6. Изменения схемы БД оформляй отдельной миграцией, совместимой с предыдущей версией. -7. Не добавляй `.env`, ключи, пароли или локальные артефакты в Git. -8. Коммиты и push выполняет только пользователь; агент никогда не коммитит и не отправляет изменения. +- Не добавляй `.env`, ключи, пароли или локальные артефакты в Git. +- Коммиты и push выполняет только пользователь; агент никогда не коммитит и не + отправляет изменения. ## Проверки diff --git a/docs/GO_STANDARD.md b/docs/GO_STANDARD.md new file mode 100644 index 0000000..cf2c3e8 --- /dev/null +++ b/docs/GO_STANDARD.md @@ -0,0 +1,275 @@ +# Стандарт Go-бэкенда + +Обязательный стандарт для Go-кода в `evening_detective_server`. Он описывает +основной вариант решения; отклонение требует разрешения пользователя и +короткого письменного обоснования в задаче или рядом с изменением. + +## 1. Основной принцип + +Пиши самый маленький код, который явно выражает бизнес-правило. Перед новым +типом, интерфейсом или пакетом проверь, нельзя ли использовать существующий +код или стандартную библиотеку. + +Перед изменением функции найди всех её callers и существующий паттерн +использования. Исправляй общее место, через которое проходят все callers, а не +только первый найденный путь. + +Не добавляй: + +- абстракцию с одной реализацией без границы подмены; +- исключение: для `internal/modules` интерфейс обязателен даже при одной + реализации; +- общий пакет для одной функции; +- конфигурацию для значения, которое не меняется; +- дублирующую валидацию в каждом handler-е; +- DTO, если существующая структура уже является контрактом нужного слоя. + +## 2. Структура и зависимости + +```text +api/ protobuf-контракт +proto/ сгенерированный Go-код +internal/app/ gRPC/HTTP handlers и mapping транспорта +internal/services// прикладные сценарии и бизнес-правила +internal/repos// PostgreSQL и модели хранения +internal/modules// переиспользуемая техническая возможность +migrations/ изменения схемы базы данных +cmd// сборка и запуск приложения +``` + +Допустимое направление зависимостей: + +```text +cmd → app → services → repos + ↘ modules +``` + +- По прикладной логике `app` зависит только от `services`; `proto` используется + как транспортный контракт. Прямые зависимости от `repos` и `modules` в + handlers и mapping запрещены. +- `cmd` собирает граф зависимостей и может импортировать `services`, `repos` и + `modules` для wiring. +- `services` не импортируют `app` и `proto`. +- `repos` не знают о protobuf, HTTP, gRPC, ролях транспорта и UI. +- `modules` не знают о конкретном use case приложения. Если код нужен только + одному сервису, он остаётся в этом сервисе. +- Циклические зависимости запрещены; не исправляй их дополнительным + `common`-пакетом без конкретной общей ответственности. + +Новый пакет получает имя по ответственности в `snake_case`, как принято в +репозитории: `game_service`, `teams_repo`, `file_storage`. Не используй +`utils`, `helpers`, `common` и `misc` для несвязанных функций. + +## 3. Handler (`internal/app`) + +Handler делает только транспортную работу: + +1. получает request и `context.Context`; +2. извлекает пользователя, роль и параметры запроса; +3. вызывает один прикладной метод сервиса; +4. преобразует результат в protobuf response; +5. возвращает ошибку через существующий gRPC/gateway-механизм. + +Handler не содержит SQL, транзакций, правил перехода состояний, расчётов и +проверок, которые должны быть одинаковыми для нескольких callers. + +Именуй методы по protobuf RPC. Вспомогательные mapping-функции держи в +отдельном `*_mapper.go` рядом с handler-ом; они не должны выполнять запросы в +базу. Данные, которые нельзя показывать конкретному caller-у, фильтруй на +границе выдачи или в сервисе — в зависимости от того, является ли это +транспортным представлением или бизнес-правилом доступа. + +```go +func (s *server) GetGame(ctx context.Context, req *proto.GetGameReq) (*proto.GetGameRsp, error) { + game, err := s.gameService.Get(ctx, req.GetId(), currentUserID(ctx)) + if err != nil { + return nil, err + } + return mapGetGameResponse(game), nil +} + +// game_mapper.go +func mapGetGameResponse(game *Game) *proto.GetGameRsp { + return &proto.GetGameRsp{Game: &proto.Game{Id: int32(game.ID), Name: game.Name}} +} +``` + +## 4. Service (`internal/services`) + +Сервис представляет один прикладной сценарий или связную область. В нём +находятся: + +- валидация входных данных и бизнес-инварианты; +- проверка прав, если она относится к правилу операции; +- orchestration нескольких repositories/modules; +- границы транзакции, когда операция должна быть атомарной; +- преобразование данных хранения в модель, нужную use case. + +Публичные методы сервиса принимают `context.Context` первым аргументом, +возвращают результат и `error`, не принимают protobuf-типы и не формируют +HTTP/gRPC-ответы. + +Называй сервис по области (`ScenarioService`), а методы — по действию +(`Create`, `Get`, `Update`, `Delete`, `List`). Не создавай интерфейс сервиса +только ради mock-а: интерфейс нужен на границе, где есть подмена или его +использует другой пакет как зависимость. Интерфейс объявляет потребитель; для +внутреннего concrete-сервиса используй структуру напрямую, если другого +варианта нет. + +Повторяющееся правило размещай в общем сервисном методе или repository, через +который проходят все callers. Не копируй guard в handlers. + +Mapping-функции не определяй внутри методов сервиса. Выноси их в отдельные +функции и файл `*_mapper.go` рядом с сервисом. Mapping только преобразует типы: +он не выполняет I/O, не меняет состояние и не принимает бизнес-решения. + +```go +// service.go +func (s *Service) Get(ctx context.Context, id int) (*Game, error) { + game, err := s.games.GetGameByID(ctx, id) + if err != nil { + return nil, err + } + return mapStoredGame(game), nil +} + +// service_mapper.go +func mapStoredGame(game *games_repo.Game) *Game { + return &Game{ID: game.ID, Name: game.Name} +} +``` + +## 5. Repository (`internal/repos`) + +Repository — единственное место для SQL и обращения к PostgreSQL. Он: + +- принимает `context.Context`; +- использует параметризованные запросы, никогда не склеивает значения в SQL; +- возвращает типы своей предметной области и ошибки; +- проверяет `rows.Err()` после итерации; +- закрывает `rows` через `defer` сразу после успешного запроса; +- переводит известные ошибки драйвера в sentinel-ошибки пакета. + +SQL держи рядом с методом, которому он нужен. Не вводи query builder и общий +репозиторный слой без повторяющейся проверенной потребности. + +Методы репозитория не проверяют права пользователя и не знают о protobuf. +Уникальность, внешние ключи и другие инварианты, которые база может обеспечить +надёжно, фиксируй также ограничением или индексом миграции. + +Изменение схемы оформляй отдельным SQL-файлом с уникальным timestamp-именем, +совместимым с предыдущей схемой. Изменение модели и миграции делай в одной +задаче. + +## 6. Modules (`internal/modules`) + +Модуль содержит техническую или переиспользуемую возможность, независимую от +конкретного прикладного сценария: хранилище файлов, JWT, отправка email, +очистка текста, генерация PDF. + +Каждый модуль разделяй на интерфейс и реализацию. Интерфейс размещай в его +пакете, реализацию держи непубличной, а конструктор возвращай как интерфейс. +Не превращай каждый маленький helper в модуль; если код уже является модулем, +для него интерфейс обязателен. + +## 7. Типы, mapping и protobuf + +- Protobuf — внешний транспортный контракт. Сгенерированные файлы в `proto/` + и Swagger вручную не редактируй. +- Новые HTTP-методы добавляй через `api/main.proto`, затем запускай генерацию + `make generate`. +- Преобразование `proto ↔ domain/storage` находится в `app` или пакете, + которому принадлежат обе модели; бизнес-правила в mapping не прячь. +- Любой mapping, принадлежащий сервису, находится в отдельной функции и файле + `*_mapper.go`, а не внутри метода сервиса. +- Модель, которой владеет один пакет, объявляй в этом пакете. Не создавай + дубликаты структуры в `internal/repos` и `internal/services` без различий + ответственности. +- Публичные типы, функции и поля документируй, если их назначение не очевидно + из имени. Непубличные имена предпочтительнее, если символ не нужен за + пределами пакета. +- Используй `int` для идентификаторов, если контракт проекта не требует + другого типа; не добавляй конвертацию без границы API или базы. + +## 8. Ошибки и context + +- Возвращай ошибки, не паникуй в обработке пользовательского ввода, БД, + файлов, сети и внешних сервисов. +- Для ожидаемых веток объявляй sentinel-ошибку в пакете, который владеет + смыслом ошибки; проверяй её через `errors.Is`. +- Оборачивай ошибку с контекстом через `%w`, например + `fmt.Errorf("загрузить сценарий %d: %w", id, err)`. +- Не сравнивай текст ошибки и не теряй исходную ошибку без причины. +- Каждый внешний вызов получает переданный `context.Context`; timeout нужен на + границе долгой операции или фонового worker-а. +- Не помещай обязательные параметры операции в context. В context допустимы + только request-scoped данные, уже принятые проектным middleware. +- Не логируй пароли, токены, персональные данные и содержимое пользовательских + файлов. + +## 9. SQL и транзакции + +- SQL-шаблоны пиши в многострочном raw string с отступами и переносами по + смысловым частям; не записывай запрос одной длинной строкой. +- Запросы должны быть читаемыми: явные поля вместо `SELECT *`, параметры вместо + конкатенации. +- Все `Query`-результаты закрывай; ошибки итерации проверяй. +- Несколько изменений, которые должны пройти вместе, выполняй в одной + транзакции. Не имитируй транзакцию последовательностью независимых вызовов. +- Не добавляй транзакцию для одной простой операции без требования атомарности. +- Конкурентные ограничения обеспечивай базой (`UNIQUE`, `CHECK`, FK) и + корректно обрабатывай конфликт в Go. + +## 10. Тесты + +Тест размещай рядом с тестируемым пакетом в файле `*_test.go`. + +- Чистые функции и mapping тестируй без базы и сети. +- Бизнес-правила сервиса тестируй через минимальные fake/stub-зависимости; + тест не должен поднимать PostgreSQL, если это не проверка SQL или транзакции. +- SQL, ограничения и атомарность проверяй интеграционным тестом с отдельной + тестовой базой через `TEST_DATABASE_URL`. +- Для endpoint-ов проверяй только транспортный mapping и доступ; полный + бизнес-сценарий принадлежит тестам сервиса. +- Имена тестов описывают поведение: `TestCreateRejectsFinishedGame`. +- Все тесты оформляй как табличные, даже если в таблице только один кейс. +- Любая новая ветка с бизнес-правилом, SQL-ограничением или безопасностью + должна иметь минимальный тест, который ломается при регрессии. + +Команды проверки: + +```sh +gofmt -w <изменённые-go-файлы> +go test ./... +``` + +После изменения protobuf дополнительно запускай `make generate` и проверяй +сгенерированный diff. + +## 11. Именование и стиль + +- Имена короткие и предметные: `gameID`, `teamID`, `ctx`, `err` допустимы по + общему Go-стилю. +- Аббревиатуры пишутся единообразно: `ID`, `URL`, `HTTP`, `JWT`, `SQL`. +- Используй префикс `I` в именах интерфейсов: `IFileStorage`, `IEmailSender`. +- Не прячь побочные эффекты в функциях с именами `Get`, `Map`, `Parse`. +- `New...` создаёт значение и возвращает ошибку только если создание реально + может завершиться ошибкой. +- Форматируй код `gofmt`; не добавляй ручное форматирование, генераторы и + линтеры без требования проекта. + +## 12. Как выбрать место для нового кода + +| Вопрос | Место | +| --- | --- | +| Как принять RPC/HTTP и вернуть protobuf? | `internal/app` | +| Какое бизнес-условие разрешает операцию? | `internal/services` | +| Как прочитать или изменить PostgreSQL? | `internal/repos` | +| Как переиспользовать S3, JWT, email или PDF? | `internal/modules` | +| Как изменить внешний контракт? | `api/`, затем генерация | +| Как изменить схему? | `migrations/` | +| Как преобразовать типы транспорта? | рядом с handler-ом или владельцем mapping | +| Как проверить поведение? | `*_test.go` рядом с владельцем поведения | + +Если ответ не помещается в одну строку, сначала уточни границу ответственности, +а не создавай новый слой.