Каковы лучшие практики по размещению (декларации) интерфейсов в Go?

Ответ

В Go существует общепринятая идиома: интерфейсы следует объявлять в пакете, который их использует (в пакете-потребителе), а не в пакете, который их реализует.

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

  1. Уменьшение связанности и избегание циклических зависимостей. Пакет-потребитель определяет, какой функционал ему нужен, и не должен зависеть от пакета с конкретной реализацией. Это позволяет легко заменять реализации.
  2. Следование принципу инверсии зависимостей (DIP). Высокоуровневые модули (потребители) не должны зависеть от низкоуровневых (реализаций). Оба должны зависеть от абстракций (интерфейсов).
  3. Гибкость и тестируемость. Позволяет легко подменять реальные зависимости на моки (mocks) или стабы (stubs) в тестах, не изменяя код потребителя.

Пример:

// Хорошо: интерфейс объявлен в пакете-потребителе (consumer)
package consumer

// Storage описывает, что нужно пакету consumer для работы с хранилищем
type Storage interface {
    Get(id string) ([]byte, error)
}

func ProcessData(s Storage) { /* ... */ }

// --- В другом пакете находится реализация ---
package db

import "some/path/to/consumer"

// DBStorage реализует интерфейс consumer.Storage
type DBStorage struct{ /* ... */ }

func (db *DBStorage) Get(id string) ([]byte, error) { /* ... */ }

Исключение из правила:
Если интерфейс является частью широко используемого публичного API библиотеки или фреймворка (например, io.Reader в стандартной библиотеке), его имеет смысл вынести в отдельный, общедоступный пакет.