Перейти к содержанию

Структура Go-сервиса

Каждый сервис живёт в отдельном git-репозитории и внутри следует одной и той же схеме папок. Это делает переключение между сервис-репо предсказуемым: где у user лежит конфиг, там и у review, и у любого нового сервиса.

Всё, что описано ниже — структура внутри одного сервис-репо. Корень дерева ниже (<service>/) — это корень клона сервис-репо на твоей машине, а не подпапка в каком-то общем репо.

Каноническая структура

<service>/                      — корень клона сервис-репо
├── cmd/
│   └── server/
│       └── main.go              — entry point: загрузка конфига, DI, запуск HTTP + worker'ов
├── internal/
│   ├── config/                  — загрузка env, валидация, DSN-хелперы
│   ├── handler/                 — HTTP-транспорт: chi-router, декодирование, кодирование
│   ├── middleware/              — chi middleware: auth, rate-limit, internal-token
│   ├── service/                 — бизнес-логика. Зависит от repository-интерфейсов
│   ├── repository/              — интерфейсы доступа к данным + postgres-реализация
│   │   └── postgres/            — конкретные реализации на pgx
│   ├── event/                   — Kafka publisher/consumer, Watermill wiring
│   ├── domain/ (или model/)     — доменные типы и sentinel-ошибки
│   └── db/                      — embed миграций и их запуск при старте
├── pkg/                         — публичные пакеты сервиса (кандидаты на вынос в shared lib)
│   ├── db/                      — pgx wrapper (InTx helper)
│   └── dto/                     — DTO для internal API, который вызывают другие сервисы
├── migrations/                  — SQL-миграции NNN_name.up.sql / .down.sql
├── docker-compose.yml           — локальный стек: postgres, redis, kafka, сам сервис
├── Dockerfile                   — multistage build, non-root runtime
├── Makefile                     — bootstrap / build / run / test / lint / up / down
├── go.mod / go.sum
└── .env.example                 — пример env-переменных (без секретов)

Референс в работе — репозиторий сервиса user. Внутри клона этого репо:

user/                            — корень клона репо user-service
├── cmd/server/main.go           — см. построение графа DI
├── internal/handler/router.go   — chi Router + middleware order
├── internal/service/*.go        — AuthService, ProfileService, AdminService
├── internal/repository/postgres — UserRepo, SessionRepo, OutboxRepo
├── pkg/db/db.go                 — pgxpool.Pool + InTx helper
└── migrations/                  — 001_init.up.sql, 002_device_id.up.sql

Что куда класть

cmd/server/main.go

  • Только wiring. Никакой бизнес-логики.
  • Загружает config, открывает pool/redis/kafka, собирает зависимости, запускает http-server и фоновые worker'ы.
  • Обрабатывает сигналы SIGINT/SIGTERM и делает graceful shutdown.
  • Если у сервиса несколько entry-point'ов (например, server и worker), — каждый кладётся как cmd/<name>/main.go.

internal/config/

  • Загрузка env через envconfig или эквивалент.
  • Валидация полей (fail-fast на старте, если secret не задан и т.п.).
  • Helper'ы типа cfg.DB.DSN() — вся склейка строк подключения живёт тут.

internal/handler/

  • chi-роутер и middleware stack.
  • Декодирование request body, валидация через go-playground/validator.
  • Вызов service-слоя. Handler никогда не обращается к repository напрямую.
  • Маппинг sentinel-ошибок из service в HTTP-статусы (mapServiceError).
  • Кодирование response.

internal/middleware/

  • Кастомные chi-middleware: JWT-authenticator, rate-limit, internal-token guard.
  • Стандартные middleware (RequestID, RealIP, Recoverer, Logger) подключаются в handler/router.go из chi/middleware, а не копируются сюда.

internal/service/

  • Бизнес-логика. Этот слой не знает про HTTP и про конкретный SQL.
  • Зависит только от repository-интерфейсов, publisher'ов и внешних клиентов — всё передаётся в конструктор.
  • Транзакции начинаются здесь через db.InTx.
  • Sentinel-ошибки (ErrUserNotFound, ErrInvalidCredentials) объявляются в service/errors.go.

internal/repository/

  • repository.go — интерфейсы (UserRepo, SessionRepo, OutboxRepo).
  • postgres/ — реализации на pgxpool.Pool.
  • Репозиторий возвращает pkgdb.ErrNotFound вместо pgx.ErrNoRows.
  • Никакого бизнес-правила в репозитории: только CRUD и специфические запросы.

internal/event/

  • Watermill publisher/subscriber, envelope, middleware stack.
  • Типы событий (структуры FooCreatedEvent), topic naming.
  • Outbox-worker (publisher через watermill-sql + components/forwarder).
  • Consumer handler'ы бизнес-событий из других сервисов.

internal/domain/ или internal/model/

  • Доменные типы (User, Review, Media). Голые struct'ы + методы.
  • Доменные sentinel-ошибки (ErrReviewNotFound).
  • Никаких зависимостей на pgx / chi / watermill.

internal/db/

  • //go:embed migrations/*.sql и helper-запуск на старте.
  • Держит embedded FS рядом с сервисом, без знаний про pgx — прокидывает в pkgdb.RunMigrations или в migrate CLI.

pkg/

Публичные пакеты сервиса — это кандидаты на вынос в отдельную shared-библиотеку, когда их API стабилизируется. Пока они живут здесь, внутри сервис-репо. Правило: не клади сюда ничего, что не является кандидатом на публичный API. В pkg/ идут:

  • pkg/db/ — pgx-wrapper (New, InTx, ErrNotFound).
  • pkg/dto/ — request/response типы для internal API, который вызывают другие сервисы по HTTP.

Всё остальное — в internal/.

Код из pkg/ одного сервис-репо нельзя импортировать из кода другого сервис-репо напрямую — это независимые репозитории. Когда такой пакет стабилизируется (API зафиксирован, есть тесты, определены владельцы), его выносят в отдельный репозиторий-библиотеку и подключают через Go-модули. До этого момента допустимо копировать код между сервис-репо; синхронизация — на владельцах.

migrations/

  • Файлы вида NNN_snake_case.up.sql и NNN_snake_case.down.sql.
  • Нумерация сплошная, трёхзначная: 001_, 002_, …, 017_.
  • Подробности — в db-pgx.md.

docker-compose.yml

  • Локальный стек: postgres, redis, kafka, сам сервис.
  • healthcheck'и обязательны для postgres/redis/kafka.
  • Dev-секреты в environment: с пометкой _dev_ / change_me.

Dockerfile

  • Multistage build: golang:1.25-alpinealpine:3.20.
  • Flags: CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w".
  • Runtime — только non-root. Эталон: Dockerfile в репозиториях сервисов user и notification:
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata && adduser -D -u 10001 app
COPY --from=builder /out/svc /bin/svc
USER app
EXPOSE 80XX
ENTRYPOINT ["/bin/svc"]

Makefile

Минимальный набор целей, которые должны быть в каждом сервисе:

  • help — список целей.
  • bootstrap — одной командой: .env + deps + docker up + миграции.
  • depsgo mod download && go mod tidy.
  • build — локальный бинарь в bin/.
  • run — запуск бинаря (ожидает, что инфра уже поднята).
  • testgo test -race -count=1 ./....
  • lintgolangci-lint run или go vet ./... как минимум.
  • up / down / clean — обёртки над docker compose.
  • logsdocker compose logs -f <service>.
  • psqldocker compose exec postgres psql -U <user> -d <db>.

Эталон — Makefile в репозитории сервиса user.

Правила зависимостей между слоями

Строгая слоевая архитектура — однонаправленные стрелки:

handler  →  service  →  repository  →  pgx
    ↓          ↓
  dto      domain / model
  • handler может импортировать service, dto, middleware.
  • service может импортировать repository (только интерфейсы!), domain, event.Publisher.
  • repository может импортировать domain, pkg/db.
  • Нет циклических импортов. Если возникает цикл — значит, слой перепутан; вытаскивай общий тип в domain/.
  • Нет импортов из service в handler. Бизнес-логика не знает про HTTP.
  • Нет импортов из repository в service. Repository не вызывает сервис.
  • Нет импортов в domain/ кроме stdlib. Доменные типы чистые.

Чего не делать

  • Не создавай internal/utils/ или internal/common/. Либо утилита относится к конкретному слою — положи её туда, либо она общая для всех сервисов — тогда pkg/<что-это-делает>/.
  • Не клади HTTP-handler рядом со service-методом. Слои разделены по папкам.
  • Не импортируй pgx в service/. Работай через repository-интерфейс.
  • Когда shared-код стабилизируется (API, тесты, владельцы), выноси его в отдельный репозиторий-библиотеку и импортируй через Go-модули. До этого момента допустимо копирование pkg/db и подобных пакетов между сервис-репо; синхронизация — задача владельцев. Не пытайся импортировать код из pkg/ чужого сервис-репо — в таком режиме это не работает.