Каков идиоматичный подход к размещению интерфейсов в Go?

Ответ

В Go действует принцип: «Принимай интерфейсы, возвращай структуры» (Accept interfaces, return structs).

Исходя из этого, интерфейсы следует определять в пакете, который их использует (потребитель/consumer), а не в пакете, который их реализует (поставщик/producer).

Этот подход дает несколько ключевых преимуществ:

  1. Отсутствие циклических зависимостей: Если бы пакет A реализовывал интерфейс из пакета B, а пакет B использовал бы тип из пакета A, возникла бы циклическая зависимость, которую компилятор Go не допустит.
  2. Слабая связанность (Decoupling): Пакет-потребитель определяет только те методы, которые ему действительно нужны. Он не зависит от всего API пакета-поставщика, а только от необходимого контракта.
  3. Ясность API пакета: Пакет-потребитель явно декларирует свои зависимости, что делает код более понятным и легким для тестирования (легко создать mock-реализацию).

Пример правильного размещения:

Предположим, у нас есть сервис пользователей (users), который зависит от хранилища (storage).

// Пакет storage (поставщик) - предоставляет конкретную реализацию
package storage

import "fmt"

// Конкретный тип, который будет реализовывать интерфейс.
// Обратите внимание, он ничего не знает об интерфейсе. ype DBStore struct {
    // ...поля для подключения к БД
}

func (s *DBStore) GetUserByID(id int) (string, error) {
    // Логика получения пользователя из БД
    return fmt.Sprintf("User %d", id), nil
}

// Пакет users (потребитель) - определяет и использует интерфейс
package users

// Интерфейс определяется здесь, в пакете-потребителе.
// Он описывает только то, что нужно сервису `users`.
type UserStorage interface {
    GetUserByID(id int) (string, error)
}

// Сервис зависит от интерфейса, а не от конкретной реализации `storage.DBStore`.
type Service struct {
    storage UserStorage
}

func NewService(s UserStorage) *Service {
    return &Service{storage: s}
}

func (s *Service) GetUserName(id int) (string, error) {
    return s.storage.GetUserByID(id)
}

Анти-паттерн: Создавать один большой пакет interfaces, куда сваливаются все интерфейсы проекта. Это нарушает принцип принадлежности интерфейса его потребителю и часто приводит к созданию "раздутых" интерфейсов.