← Назад к вопросам

Где лучше хранить интерфейсы?

1.0 Junior🔥 121 комментариев
#Основы Go

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Где лучше хранить интерфейсы в 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) {}

Рекомендации на практике

  1. Начинайте с подхода "рядом с потребителем" — он наиболее идиоматичен для Go и соответствует принципу "Accept interfaces, return structs".
  2. Для межмодульного взаимодействия в чистой архитектуре (Clean/Hexagonal) выделяйте интерфейсы портов в отдельный слой (ports).
  3. В библиотеках помещайте интерфейсы рядом с реализацией, но старайтесь делать их минималистичными.
  4. Избегайте размещения интерфейсов в пакете models или entities — это смешивает уровни абстракции.
  5. Используйте кодогенерацию (например, go:generate mockery) для автоматического создания моков, если интерфейсов много.

Пример для типичного сервиса:

/internal
  /app           (логика приложения)
    /usecase
      /user_usecase.go   ← здесь интерфейс UserRepo
  /domain        (общие контракты)
    /repository.go
  /ports         (интерфейсы для внешних систем)
    /http.go
    /repository.go
  /infrastructure
    /persistence
      /postgresql.go   ← реализация интерфейса из domain

Ключевой принцип: Интерфейс должен принадлежать тому, кто его использует, а не тому, кто его реализует. Это снижает связанность и делает код более тестируемым и гибким. Выбор конкретного подхода зависит от масштаба проекта и принятой архитектурной парадигмы.