Пост

Еще один вариант архитектуры проекта на Go

Подход к организации сервисов с разделением на уровни ответственности

Общая структура проекта

1
2
3
4
5
6
7
8
9
cmd/                # точка входа в приложение
config/             # конфигурация
internal/           # основная бизнес-логика
    entities/       # доменные сущности и интерфейсы
    usecases/       # сценарии (application layer)
    infrastructure/ # адаптеры, работа с БД, внешние сервисы
    interfaces/     # входные интерфейсы (HTTP, gRPC, CLI и т.д.)
    services/       # вспомогательные пакеты (опционально)
pkg/                # переиспользуемые библиотеки

Разбор по директориям

cmd/ — точка инициализации приложения

Здесь находятся исполняемые пакеты. Каждый сервис/микросервис обычно имеет свою поддиректория:

1
2
3
cmd/
    app/    # main.go — запуск API
    worker/ # main.go — запуск воркера

Внутри — только инициализация зависимостей и запуск. Бизнес-логики здесь быть не должно.


config/ — конфигурация

Хранение yaml/json файлов, переменных окружения, а также структуры для конфигов. Обычно используется вместе с библиотеками вроде viper или envconfig.


internal/entities/ — доменные сущности

Содержит чистые структуры и интерфейсы, описывающие предметную область.

Пример:

1
2
3
4
5
6
7
// internal/entities/team.go
package entities

type Team struct {
    ID   int64
    Name string
}

Тут нет логики работы с БД или HTTP — только модели и интерфейсы.


internal/usecases/ — бизнес-правила и сценарии

Здесь описываются интерфейсы взаимодействия между сущностями и реализация сценариев.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// internal/usecases/team_usecase.go
package usecases

import "myapp/internal/entities"

type TeamRepository interface {
    Create(team entities.Team) error
    GetByID(id int64) (*entities.Team, error)
}

type TeamUseCase struct {
    repo TeamRepository
}

func NewTeamUseCase(repo TeamRepository) *TeamUseCase {
    return &TeamUseCase{repo: repo}
}

func (uc *TeamUseCase) RegisterTeam(name string) (*entities.Team, error) {
    team := entities.Team{Name: name}
    if err := uc.repo.Create(team); err != nil {
        return nil, err
    }
    return &team, nil
}

UseCase не знает, где хранятся данные (БД, память, внешний сервис) и как именно приходит запрос (HTTP, gRPC).


internal/infrastructure/ — реализация адаптеров

Тут лежат конкретные реализации интерфейсов для работы с внешними системами:

  • Репозитории для БД (PostgreSQL, Redis и т.д.)
  • Адаптеры для сторонних API
  • Логгеры, очереди, кэш
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// internal/infrastructure/postgres/team_repo.go
package postgres

import (
    "database/sql"
    "myapp/internal/entities"
    "myapp/internal/usecases"
)

type TeamRepo struct {
    db *sql.DB
}

func NewTeamRepo(db *sql.DB) *TeamRepo {
    return &TeamRepo{db: db}
}

var _ usecases.TeamRepository = (*TeamRepo)(nil)

func (r *TeamRepo) Create(team entities.Team) error {
    _, err := r.db.Exec(`INSERT INTO teams (name) VALUES ($1)`, team.Name)
    return err
}

func (r *TeamRepo) GetByID(id int64) (*entities.Team, error) {
    row := r.db.QueryRow(`SELECT id, name FROM teams WHERE id = $1`, id)
    var team entities.Team
    if err := row.Scan(&team.ID, &team.Name); err != nil {
        return nil, err
    }
    return &team, nil
}

internal/interfaces/ — внешние интерфейсы

Слой взаимодействия с пользователем или другими сервисами: HTTP, gRPC, CLI.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// internal/interfaces/http/handlers/team_handler.go
package http

import (
    "encoding/json"
    "net/http"
    "myapp/internal/usecases"
)

type TeamHandler struct {
    uc *usecases.TeamUseCase
}

func NewTeamHandler(uc *usecases.TeamUseCase) *TeamHandler {
    return &TeamHandler{uc: uc}
}

func (h *TeamHandler) CreateTeam(w http.ResponseWriter, r *http.Request) {
    var req struct{ Name string }
    _ = json.NewDecoder(r.Body).Decode(&req)
    team, err := h.uc.RegisterTeam(req.Name)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(team)
}

internal/services/ (опционально)

Используется, если между usecases и инфраструктурой нужно добавить слой вспомогательной логики (например, сервис валидации, работа с кэшем).


pkg/ — переиспользуемые библиотеки

Здесь могут находиться хелперы, утилиты или пакеты, которые теоретически можно вынести в отдельный репозиторий и использовать в других проектах.

Например: кастомный логгер, middleware для HTTP, библиотека для работы с конфигами.


Преимущества подхода

  1. Разделение ответственности — код проще читать и поддерживать.
  2. Тестируемость — usecases довольно просто покрыть unit-тестами, подменяя интерфейсы.
  3. Гибкость — можно менять БД или протокол общения (HTTP → gRPC) без переписывания остальной логики.
  4. Масштабируемость — структура подходит как для небольших сервисов, так и для больших систем.

Итог

Такая структура помогает строить Go-приложения по некоторым из принципов чистой архитектуры и DDD. Она подходит для проектов, где важна долгосрочная поддержка и развитие.

Больше полезной информации в Telegram-канале