# Стандарт 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 ``` - `modules` — технический слой, доступный из любого уровня (`cmd`, `app`, `services`, `repos`). `proto` используется как транспортный контракт. - Прямые зависимости `app` от `repos` запрещены в production-коде handlers и mapping; прикладные операции идут через `services`. - `cmd//main.go` — composition root: для инициализации и wiring он может импортировать любой внутренний слой (`app`, `services`, `repos`, `modules`). - `services` не импортируют `app` и `proto`. - `repos` не знают о protobuf, HTTP, gRPC, ролях транспорта и UI. - `modules` не импортируют `app`, `services` и `repos`, не знают о конкретном 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 вручную не редактируй. - В protobuf используй `ID`, `ID` и `URL` для полей с соответствующими аббревиатурами. Генератор тогда создаёт `ID`, `GameID`, `URL` в Go и `ID`, `gameID`, `URL` в TypeScript. - Поля каждого `message` объявляй в порядке возрастания номеров. Номера уже опубликованных полей не меняй и не переиспользуй; удалённые номера оставляй зарезервированными. - Новые HTTP-методы добавляй через `api/main.proto`, затем запускай генерацию `make generate`. - Преобразование `proto ↔ domain/storage` находится в `app` или пакете, которому принадлежат обе модели; бизнес-правила в mapping не прячь. - Поля структур в mapping-литералах располагай вертикально: одно поле на строку. - Любой mapping, принадлежащий сервису, находится в отдельной функции и файле `*_mapper.go`, а не внутри метода сервиса. - Модель, которой владеет один пакет, объявляй в этом пакете. Не создавай дубликаты структуры в `internal/repos` и `internal/services` без различий ответственности. - Публичные типы, функции и поля документируй, если их назначение не очевидно из имени. Непубличные имена предпочтительнее, если символ не нужен за пределами пакета. - Используй `int` для идентификаторов, если контракт проекта не требует другого типа; не добавляй конвертацию без границы API или базы. ## 8. Ошибки и context - Возвращай ошибки, не паникуй в обработке пользовательского ввода, БД, файлов, сети и внешних сервисов. `panic` допустим только для невозможного нарушения внутреннего инварианта при старте или в коде, который не имеет осмысленного способа продолжить работу. - Ошибкой владеет пакет, которому принадлежит её смысл. Ожидаемые состояния объявляй как sentinel-ошибки (`var Err... = errors.New(...)`) в сервисе, repository или module; не дублируй одну смысловую ошибку в разных слоях. - Repository переводит ошибки драйвера и ограничения БД в свои предметные ошибки. Service переводит их в ошибки use case. `app` не меняет бизнес-смысл ошибки, а только преобразует её в транспортный ответ. - Для ожидаемых веток проверяй цепочку через `errors.Is`; для ошибок с данными используй `errors.As`. Текст ошибки не сравнивай. - Оборачивай ошибку с контекстом через `%w`, например `fmt.Errorf("загрузить сценарий %d: %w", id, err)`. Не теряй исходную ошибку без причины и не оборачивай её повторно пустым или одинаковым сообщением. - `status.Error` и `status.Errorf` используй только на транспортной границе (`internal/app` или gRPC middleware). Services, repositories и modules не возвращают gRPC-ошибки. - Преобразование ошибок use case в gRPC-коды держи в `*_errors.go` рядом с handler-ом. Для неизвестной ошибки возвращай `codes.Internal` с безопасным общим сообщением; детали причины сохраняй в цепочке и логируй на внешней границе. - Клиенту нельзя отдавать SQL, пути файлов, адреса внешних сервисов, stack trace, токены, пароли или персональные данные. Ошибки валидации могут содержать только безопасные сведения, необходимые клиенту. - Логируй ошибку один раз на границе, которая принимает решение о внешнем ответе; нижние слои добавляют контекст через `%w`, но не дублируют лог. - Каждый внешний вызов получает переданный `context.Context`; timeout нужен на границе долгой операции или фонового worker-а. Отмену контекста не скрывай: возвращай `ctx.Err()` или оборачивай её через `%w`. - Не помещай обязательные параметры операции в context. В context допустимы только request-scoped данные, уже принятые проектным middleware. - Для каждой новой ветки бизнес-ошибки тестируй `errors.Is` и итоговый транспортный код; для неизвестной ошибки проверяй, что наружу уходит `codes.Internal` без внутренних деталей. ## 9. SQL и транзакции - SQL-шаблоны пиши в многострочном raw string с отступами и переносами по смысловым частям; не записывай запрос одной длинной строкой. - Вызов `Query`, `QueryRow` или `Exec` оформляй многострочно: сначала `ctx`, затем SQL, затем каждый аргумент запроса на отдельной строке и закрывающая скобка. - В `SELECT` перечисляй поля вертикально; `FROM`, `JOIN`, `WHERE`, `ORDER BY`, `GROUP BY`, `ON CONFLICT` и `RETURNING` начинай с отдельных строк. Не склеивай несколько SQL-клауз в одну строку. - Запросы должны быть читаемыми: явные поля вместо `SELECT *`, параметры вместо конкатенации. - Все `Query`-результаты закрывай; ошибки итерации проверяй. - Несколько изменений, которые должны пройти вместе, выполняй в одной транзакции. Не имитируй транзакцию последовательностью независимых вызовов. - Не добавляй транзакцию для одной простой операции без требования атомарности. - Конкурентные ограничения обеспечивай базой (`UNIQUE`, `CHECK`, FK) и корректно обрабатывай конфликт в Go. ## 10. Тесты Тест размещай рядом с тестируемым пакетом в файле `*_test.go`. - Чистые функции и mapping тестируй без базы и сети. - Бизнес-правила сервиса тестируй через минимальные fake/stub-зависимости; тест не должен поднимать PostgreSQL, если это не проверка SQL или транзакции. - SQL, ограничения и атомарность проверяй интеграционным тестом с отдельной тестовой базой через `TEST_DATABASE_URL`. - Не пиши SQL-запросы в тестовом сценарии и не обращайся к базе напрямую через `Query`, `QueryRow` или `Exec`. Подготовку данных и проверки выполняй через реальные публичные методы тестируемого сервиса или repository. Схему можно подготовить миграциями инфраструктурного слоя, но тест не должен подменять ими методы приложения или проверять результат прямым SQL-запросом. - Для endpoint-ов проверяй только транспортный mapping и доступ; полный бизнес-сценарий принадлежит тестам сервиса. - Имена тестов описывают поведение: `TestCreateRejectsFinishedGame`. - Все тесты оформляй как табличные, даже если в таблице только один кейс. - Любая новая ветка с бизнес-правилом, SQL-ограничением или безопасностью должна иметь минимальный тест, который ломается при регрессии. Команды проверки: ```sh gofmt -w <изменённые-go-файлы> go test ./... ``` После изменения protobuf дополнительно запускай `make generate` и проверяй сгенерированный diff. ## 11. Именование и стиль - Все комментарии в исходном Go-коде пиши на русском языке. Сгенерированные файлы и комментарии внешних библиотек не редактируй. - Имена короткие и предметные: `gameID`, `teamID`, `ctx`, `err` допустимы по общему Go-стилю. - Аббревиатуры пишутся единообразно: `ID`, `URL`, `HTTP`, `JWT`, `SQL`. - Используй префикс `I` в именах интерфейсов: `IFileStorage`, `IEmailSender`. - Не прячь побочные эффекты в функциях с именами `Get`, `Map`, `Parse`. - Сервисы и конструкторы называй по предметной области: `ScreenService` и `NewScreenService`; generic-имена `Service` и `New` запрещены. - `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` рядом с владельцем поведения | Если ответ не помещается в одну строку, сначала уточни границу ответственности, а не создавай новый слой.