Как принято структурировать слой доступа к данным (Data Access Layer) в Go-проектах?

Ответ

В Go нет единого строгого фреймворка, поэтому структура проекта определяется соглашениями. Слой доступа к данным, часто реализуемый через паттерн "Репозиторий" (Repository), принято размещать в следующих директориях:

  1. internal/repository (или internal/storage): Самый распространенный вариант. Код в internal не может быть импортирован другими проектами, что обеспечивает инкапсуляцию логики доступа к данным внутри вашего сервиса.

  2. pkg/repository: Используется, если вы пишете библиотеку, и этот код предназначен для переиспользования в нескольких проектах. Для бизнес-приложений это менее частый случай.

  3. db/ или storage/ (в корне проекта): Иногда используется в небольших проектах для простоты.

Пример структуры:

/my-project
├── cmd/app/main.go
├── internal/
│   ├── service/      # Бизнес-логика
│   │   └── user_service.go
│   ├── repository/   # Слой доступа к данным
│   │   └── user_repository_postgres.go
│   └── models/       # Модели данных
│       └── user.go
└── pkg/
    └── db/           # Логика подключения к БД
        └── postgres.go

Пример кода репозитория:

package repository

import (
    "database/sql"
    "my-project/internal/models"
)

// UserRepository определяет интерфейс для работы с пользователями
// Это позволяет легко подменять реализацию (например, для тестов).
type UserRepository interface {
    GetByID(id int) (*models.User, error)
}

// UserPostgresRepo - конкретная реализация для PostgreSQL.
type UserPostgresRepo struct {
    db *sql.DB // Зависимость от подключения к БД
}

func NewUserPostgresRepo(db *sql.DB) *UserPostgresRepo {
    return &UserPostgresRepo{db: db}
}

func (r *UserPostgresRepo) GetByID(id int) (*models.User, error) {
    // ... логика запроса к БД ...
    return nil, nil
}

Ключевая цель такого разделения — изоляция бизнес-логики от деталей реализации хранилища. Это упрощает тестирование (можно подменить репозиторий моком), сопровождение и возможную замену СУБД в будущем.

Ответ 18+ 🔞

А, ну вот, опять про эту вашу модульную архитектуру в Go! Слушай, а ты знаешь, что в Go нет единого, блядь, строгого фреймворка, который бы тебе всё на блюдечке принёс? Нет, сука! Поэтому структуру проекта ты определяешь сам, по соглашениям, как умный мальчик.

Вот этот слой доступа к данным, который часто делают через паттерн «Репозиторий» (Repository), его, блядь, принято пихать в определённые директории. Смотри сюда, я тебе сейчас всё разложу, как на духу.

  1. internal/repository (или internal/storage) — это, блядь, самый ходовой вариант! Весь код в internal — это святая святых, его другие проекты импортировать не могут. Полная инкапсуляция, ебать! Вся твоя логика доступа к данным сидит внутри сервиса, как в бункере.

  2. pkg/repository — это уже для распиздяев, которые библиотеки пишут. Если твой код должен переиспользоваться в других проектах — вот тебе pkg. Но для обычного бизнес-приложения это, блядь, редкость, честно говоря.

  3. db/ или storage/ прямо в корне проекта — это для совсем маленьких проектов, где можно по-простому, без этих ваших заморочек.

Пример структуры, на которую можно посмотреть и сказать: «О, ёпта, норм!»

/my-project
├── cmd/app/main.go
├── internal/
│   ├── service/      # Тут у нас бизнес-логика, мозги проекта
│   │   └── user_service.go
│   ├── repository/   # А вот это, блядь, слой доступа к данным! Хранилище!
│   │   └── user_repository_postgres.go
│   └── models/       # Модели данных, сущности
│       └── user.go
└── pkg/
    └── db/           # А тут логика подключения к самой БД
        └── postgres.go

А вот тебе пример кода репозитория, чтобы ты понимал, о чём речь

package repository

import (
    "database/sql"
    "my-project/internal/models"
)

// UserRepository — это интерфейс для работы с пользователями, ёпта!
// Зачем? А затем, что это позволяет легко подменить реализацию, например, для тестов. Хитро, да?
type UserRepository interface {
    GetByID(id int) (*models.User, error)
}

// UserPostgresRepo — конкретная реализация для PostgreSQL. Вот она, родная!
type UserPostgresRepo struct {
    db *sql.DB // Зависимость от подключения к БД, как же без неё
}

func NewUserPostgresRepo(db *sql.DB) *UserPostgresRepo {
    return &UserPostgresRepo{db: db}
}

func (r *UserPostgresRepo) GetByID(id int) (*models.User, error) {
    // ... а тут, блядь, логика запроса к БД, которую ты сам и напишешь ...
    return nil, nil
}

И главная цель всей этой хуйни — изоляция бизнес-логики от деталей реализации хранилища! Это же, блядь, гениально просто: упрощает тестирование (можно репозиторий моком подменить), сопровождение и, если вдруг надумаешь СУБД менять, то не придётся весь проект переписывать. В рот меня чих-пых, какая красота!