Где и почему в Go принято объявлять интерфейсы?

Ответ

В Go существует идиома: «Интерфейсы принадлежат потребителю». Это означает, что интерфейсы следует объявлять в пакете, который их использует, а не в пакете, который их реализует.

Почему это важно?


  1. Уменьшение связанности (Decoupling): Пакет, предоставляющий реализацию, не должен знать о своих потребителях. Это предотвращает циклические зависимости и делает компоненты системы более независимыми. Потребитель определяет контракт, который ему нужен, а реализатор его выполняет.



  2. Принцип разделения интерфейсов (Interface Segregation Principle): Потребитель определяет только тот набор методов, который ему действительно необходим. Это приводит к созданию маленьких, сфокусированных интерфейсов, вместо одного большого и громоздкого.



  3. Простота тестирования и мокирования: Когда интерфейс находится в пакете-потребителе, для его тестирования легко создать заглушку (mock), не импортируя при этом пакет с реальной, возможно, тяжеловесной реализацией.


Пример:

Предположим, у нас есть пакет notifier, который отправляет уведомления, и пакет database для работы с БД.

// Пакет notifier (потребитель)
// Он не знает о существовании database, ему нужен лишь кто-то, 
// кто может предоставить данные пользователя.
package notifier

// UserFinder - интерфейс, определенный потребителем.
type UserFinder interface {
    FindUserByID(id int) (string, error)
}

func NotifyUser(finder UserFinder, userID int) {
    userName, err := finder.FindUserByID(userID)
    // ... логика отправки уведомления
}
// Пакет database (реализатор)
// Этот пакет ничего не знает об интерфейсе UserFinder.
// Он просто предоставляет конкретный тип с нужным методом.
package database

type PostgresDB struct { /* ... */ }

// Метод полностью соответствует сигнатуре интерфейса UserFinder.
func (db *PostgresDB) FindUserByID(id int) (string, error) {
    // ... логика поиска пользователя в БД
    return "John Doe", nil
}

Такой подход делает код более гибким, модульным и легко поддерживаемым.