add standart

This commit is contained in:
2026-10-01 13:03:14 +07:00
parent 413c2d4497
commit 76af14cb96
2 changed files with 282 additions and 17 deletions
+275
View File
@@ -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/<name>/ прикладные сценарии и бизнес-правила
internal/repos/<name>/ PostgreSQL и модели хранения
internal/modules/<name>/ переиспользуемая техническая возможность
migrations/ изменения схемы базы данных
cmd/<name>/ сборка и запуск приложения
```
Допустимое направление зависимостей:
```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` рядом с владельцем поведения |
Если ответ не помещается в одну строку, сначала уточни границу ответственности,
а не создавай новый слой.