Структура 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или вmigrateCLI.
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-alpine→alpine: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 + миграции.deps—go mod download && go mod tidy.build— локальный бинарь вbin/.run— запуск бинаря (ожидает, что инфра уже поднята).test—go test -race -count=1 ./....lint—golangci-lint runилиgo vet ./...как минимум.up/down/clean— обёртки надdocker compose.logs—docker compose logs -f <service>.psql—docker compose exec postgres psql -U <user> -d <db>.
Эталон — Makefile в репозитории сервиса user.
Правила зависимостей между слоями¶
Строгая слоевая архитектура — однонаправленные стрелки:
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/чужого сервис-репо — в таком режиме это не работает.