Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Где лучше хранить интерфейсы в Go проекте?
В Go нет строгих правил на этот счет, но существуют проверенные практики, которые зависят от архитектурного контекста, степени абстракции и зоны ответственности интерфейса. Вот основные подходы:
1. Рядом с потребителем (Consumer-first / Client-side)
Интерфейс объявляется в пакете, где он используется, а не там, где реализуется. Это классический принцип "клиент определяет контракт" из интерфейсного сегрегации.
// в пакете consumer (например, app/usecase)
package usecase
type UserRepository interface {
FindByID(id string) (*User, error)
Save(user *User) error
}
func ProcessUser(repo UserRepository, id string) error {
user, err := repo.FindByID(id)
// ...
}
Плюсы:
- Низкая связанность: Потребитель зависит только от нужного ему контракта, а не от всего пакета реализации.
- Гибкость: Легко подменить реализацию (например, мок для тестов) без модификации пакета-поставщика.
- Ясность: Четко видно, какие методы действительно используются.
Минусы:
- Может привести к дублированию интерфейсов, если несколько потребителей определяют схожие контракты.
2. Рядом с реализацией (Implementation-side)
Интерфейс объявляется в том же пакете, что и его основная реализация. Это часто встречается в библиотеках и при инверсии зависимостей (DIP), когда высокоуровневые модули определяют интерфейсы, а низкоуровневые — реализуют их.
// в пакете поставщика (например, pkg/storage/mysql)
package mysql
type UserStorage interface {
GetUser(id string) (*User, error)
StoreUser(user *User) error
}
type MySQLStorage struct{}
func (s *MySQLStorage) GetUser(id string) (*User, error) {
// реализация для MySQL
}
Плюсы:
- Централизация: Все связанные абстракции и их реализации находятся вместе.
- Удобство для библиотек: Пользователи библиотеки работают с готовыми контрактами.
Минусы:
- Риск "загрязнения" интерфейса методами, нужными только внутренней реализации.
- Потребитель вынужден импортировать пакет с реализацией, даже если использует только интерфейс.
3. В отдельном нейтральном пакете (Shared package)
Для общедоменных (ubiquitous) интерфейсов, которые используются многими компонентами системы, создается отдельный пакет (например, pkg/domain, internal/ports, app/contracts).
// в pkg/domain/repository.go
package domain
type UserRepository interface {
FindByID(id string) (*User, error)
Save(user *User) error
}
// в app/usecase - потребитель
package usecase
import "project/pkg/domain"
func ProcessUser(repo domain.UserRepository) {}
// в infra/persistence - реализация
package persistence
import "project/pkg/domain"
type PostgreSQLRepo struct{}
func (r *PostgreSQLRepo) FindByID(id string) (*domain.User, error) {}
Плюсы:
- Ясная архитектура: Четкое разделение слоев (domain, application, infrastructure).
- Избегание циклических зависимостей: Нейтральный пакет может импортироваться всеми.
- Единая точка правки для ключевых контрактов.
Минусы:
- Риск создания "божественного" пакета (God package), куда складывают всё подряд.
- Дополнительная сложность для небольших проектов.
4. Внутренние (Private) интерфейсы
Интерфейсы, используемые только внутри одного пакета для организации кода (например, стратегия, фасад), должны быть неэкспортируемыми.
package cache
type evictionStrategy interface { // неэкспортируемый!
Evict(c *Cache)
}
type lruStrategy struct{}
func (s *lruStrategy) Evict(c *Cache) {}
Рекомендации на практике
- Начинайте с подхода "рядом с потребителем" — он наиболее идиоматичен для Go и соответствует принципу "Accept interfaces, return structs".
- Для межмодульного взаимодействия в чистой архитектуре (Clean/Hexagonal) выделяйте интерфейсы портов в отдельный слой (
ports). - В библиотеках помещайте интерфейсы рядом с реализацией, но старайтесь делать их минималистичными.
- Избегайте размещения интерфейсов в пакете
modelsилиentities— это смешивает уровни абстракции. - Используйте кодогенерацию (например,
go:generate mockery) для автоматического создания моков, если интерфейсов много.
Пример для типичного сервиса:
/internal
/app (логика приложения)
/usecase
/user_usecase.go ← здесь интерфейс UserRepo
/domain (общие контракты)
/repository.go
/ports (интерфейсы для внешних систем)
/http.go
/repository.go
/infrastructure
/persistence
/postgresql.go ← реализация интерфейса из domain
Ключевой принцип: Интерфейс должен принадлежать тому, кто его использует, а не тому, кто его реализует. Это снижает связанность и делает код более тестируемым и гибким. Выбор конкретного подхода зависит от масштаба проекта и принятой архитектурной парадигмы.