Как я сейчас структурирую сервис на Go
Когда я только начал писать backend-сервисы на Go, больше всего я путался не в синтаксисе, а в структуре проекта.
Что такое «бизнес-логика»? Зачем нужна директория infrastructure? Куда класть ошибки, логгер, модели данных?
Ситуацию усложняло то, что каждый автор статьи или видео показывал свой вариант. Подходов к структуре Go-сервиса существует множество. Одной идеальной структуры не существует — всё зависит от проекта и задач.
Со временем у меня выработался свой базовый шаблон, к которому я возвращаюсь снова и снова.
Обычно я использую эту структуру для сервисов среднего размера: когда есть HTTP API, база данных, несколько доменных модулей и отдельные сервисы с бизнес-логикой. Для маленьких pet-проектов она может быть избыточной.
В этой статье расскажу, как я сейчас организую сервис на Go (на примере своих проектов).
Верхний уровень проекта
Примерная структура корня проекта:
myapp/
├── cmd/
├── configs/
├── internal/
├── migrations/
├── pkg/
├── tests/
├── web/
├── go.mod
├── go.sum
└── Makefile
Это не «единственно правильная» структура, а рабочий шаблон, который помогает не тонуть в хаосе.
Что лежит в корне
cmd
Точка входа в приложение.
Для каждого бинарника — своя подпапка с main.go.
Например:
cmd/
├── server/
│ └── main.go
└── bot/
└── main.go
Отдельно для HTTP-сервера и Telegram-бота.
Такой подход удобен, когда у приложения несколько точек входа: например API-сервер, воркер или CLI-утилита.
configs
Конфиги приложения: порты, адреса сервисов, настройки HTTP-сервера и т.п.
Типичный пример:
http:
addr: ":8080"
db:
host: "localhost"
port: 5432
user: "app"
name: "app"
Иногда добавляю конфиги для разных окружений:
configs/
├── config.yml
├── config.dev.yml
└── config.prod.yml
Это позволяет менять параметры (например адреса сервисов или уровни логирования) без изменения кода.
Секреты (пароли, токены) сюда не кладу — они живут в .env.
Если всё-таки складывать секреты в YAML, файл нужно добавлять в .gitignore и держать рядом config.example.yml для примера.
migrations
SQL-миграции для баз данных: создание таблиц, изменения схемы, добавление новых полей.
migrations/
├── 20260302090925_create_users.sql
├── 20260302120917_create_posts.sql
├── 20260302121545_create_comments.sql
└── 20260306120308_create_refresh_tokens.sql
С этого обычно начинается инфраструктура любого более-менее серьёзного сервиса.
pkg
Код, который я потенциально могу переиспользовать в других проектах.
Например:
- телеграм-клиент для отправки сообщений;
- retry / backoff утилиты;
- небольшой HTTP-клиент с таймаутами и ретраями;
- helpers работы со временем.
Пример:
pkg/
└── telegram/
├── client.go
└── client_test.go
Если понимаю, что пакет живёт только внутри конкретного приложения, оставляю его в internal, а не в pkg.
tests
Интеграционные тесты и тестовые данные.
tests/
└── integration/
└── repository/
├── user_test.go
├── post_test.go
Здесь обычно лежат тесты, которые поднимают реальную базу данных, HTTP-сервер или другие внешние зависимости.
web
Фронтенд или статика: HTML-лендинг, SPA (React/Vue) и прочее.
Например:
web/
├── index.html
├── assets/
└── dist/
Самое интересное — internal
Вся внутренняя логика приложения, которая не должна импортироваться снаружи.
Папка internal — это особенность Go.
Пакеты внутри неё нельзя импортировать из других модулей, поэтому она помогает явно отделить внутренний код приложения от публичных библиотек.
Базовый скелет:
internal/
├── app/
├── config/
├── domain/
├── infra/
├── logger/
├── service/
└── delivery/
Разберём папки по очереди.
internal/config
Здесь я собираю конфигурацию: читаю config.yml + .env, валидирую и отдаю уже готовую структуру.
internal/
└── config/
└── config.go
Идея в том, чтобы остальные части кода работали с нормальной Go-структурой, а не парсили YAML или ENV каждый раз сами.
internal/logger
Единая настройка логгера: уровни, формат, вывод, поля по умолчанию.
internal/
└── logger/
└── logger.go
Логгер инициализируется один раз и дальше прокидывается по слоям, а не создаётся «по месту» в каждом пакете.
internal/domain
Бизнес-модели: пользователи, посты, комментарии и т.п.
Здесь — только данные и бизнес-смысл, без знания о БД, HTTP или Telegram.
Пример:
internal/
└── domain/
├── user.go
├── refresh_token.go
├── post.go
└── comment.go
Простейшая модель может выглядеть так:
type User struct {
ID int64
Email string
Password string
CreatedAt time.Time
}
В domain-моделях обычно нет SQL-тегов, JSON-тегов и прочих инфраструктурных деталей.
Domain-слой описывает что существует в предметной области.
internal/infra
Доступ к данным и внешним сервисам: БД, кэш, файловые хранилища, брокеры сообщений и т.д.
Пример:
internal/
└── infra/
├── postgres/
│ ├── pool.go
│ ├── errors.go
│ ├── post_repository.go
│ ├── user_repository.go
│ └── refresh_token_repository.go
├── redis/
│ └── ...
└── kafka/
└── ...
Обычно здесь реализуются репозитории — адаптеры, которые переводят операции сервиса в конкретные запросы к базе данных.
То есть сервис говорит:
“создай пользователя”
а repository превращает это в SQL-запрос.
Например.
Интерфейс, который нужен сервису:
// internal/service/user/interfaces.go
type UserRepository interface {
GetByID(ctx context.Context, id int64) (*domain.User, error)
}
А реализация находится в infra:
// internal/infra/postgres/user_repository.go
type UserRepo struct {
db *pgxpool.Pool // или *sql.DB
}
func (r *UserRepo) GetByID(ctx context.Context, id int64) (*domain.User, error) {
var user domain.User
query := `SELECT id, name FROM users WHERE id = $1`
err := r.db.QueryRow(ctx, query, id).
Scan(&user.ID, &user.Name)
return &user, err
}
Так сервис зависит от интерфейса, а не от конкретной базы данных. Благодаря этому можно, например, заменить PostgreSQL на другую БД или подставить мок в тестах.
Важно
Интерфейсы следует помещать в пакет, где они используются, а не в пакет, где реализуются. Это упрощает читаемость кода и снижает зависимость пакетов друг от друга.
internal/service
Бизнес-логика поверх домена и репозиториев.
Здесь живут use-case’ы: авторизация, создание постов, комментарии, лайки и т.п.
Пример структуры модуля:
internal/
└── service/
├── auth/
│ ├── errors.go
│ ├── interfaces.go
│ ├── mocks.go
│ ├── service.go
│ └── service_test.go
├── post/
│ ├── errors.go
│ ├── interfaces.go
│ ├── mocks.go
│ ├── service.go
│ └── service_test.go
Отдельные файлы вроде errors.go, interfaces.go позволяют держать основной service.go чище и читабельнее.
internal/delivery
Транспортный слой: HTTP-хендлеры, gRPC, Telegram-бот и всё, что отвечает за входящие запросы.
Пример:
internal/
└── delivery/
├── http/
│ ├── handlers/
│ ├── router.go
│ └── server.go
└── telegram/
├── handlers/
├── router.go
├── keyboards.go
└── dispatcher.go
Задача delivery — принять запрос, разобрать его, вызвать нужный сервис и вернуть ответ, не зная, как именно сервис хранит данные.
internal/app
Сборка приложения: инициализация зависимостей, запуск серверов, воркеров и фоновых задач.
internal/
└── app/
├── shared.go
├── server.go
└── bot.go
Здесь создаются инстансы конфигов, логгера, БД, репозиториев и сервисов, после чего они прокидываются в delivery.
По сути, это точка сборки всего приложения.
Про зависимости между слоями
Я стараюсь держаться простого правила:
- domain не знает про HTTP, SQL, Redis и т.п.;
- service говорит с infra через интерфейсы;
- delivery обращается к service;
- app собирает всё вместе.
Схематично это выглядит так:
delivery -> service -> domain
|
infra
Например, HTTP-хендлер не должен напрямую работать с базой данных.
Так легче менять части системы: например заменить PostgreSQL на другую БД или переписать HTTP-слой, не трогая домен и сервисы.
Итоговая структура (пример)
Если собрать всё вместе, типичный сервис у меня сейчас выглядит так:
myapp/
├── cmd/
│ └── app/
│ └── main.go
├── configs/
│ └── config.yml
├── internal/
│ ├── app/
│ ├── config/
│ ├── domain/
│ ├── infra/
│ ├── logger/
│ ├── service/
│ └── delivery/
├── migrations/
├── pkg/
├── tests/
└── web/
Это не стандарт из книжки, а рабочий каркас, который можно адаптировать под свои задачи.
В разных проектах и технологиях расположение и названия директорий могут отличаться:
delivery -> transport
handlers -> controllers
domain -> models / entities
service -> usecase
Главное — понимать, зачем нужен каждый слой, а не копировать структуру просто потому, что «так делают все».
Когда структура может быть проще
Если проект маленький (1–2 эндпоинта и одна таблица), я обычно не создаю все эти директории.
Иногда достаточно такой структуры:
internal/
handler/
repository/
service/
А всё остальное появляется по мере роста проекта.
Заключение
Со временем структура почти всегда эволюционирует: появляются новые сервисы, воркеры, очереди, фоновые задачи.
Но если изначально разделить код на домен, сервисы, инфраструктуру и транспорт, проект гораздо легче масштабировать и поддерживать.
Именно поэтому я чаще всего начинаю новые Go-сервисы с такого каркаса.