Ответ
Интерфейсы в Go — это мощный инструмент для создания гибкого и слабосвязанного кода. Они определяют поведение, а не данные.
// Определяем интерфейс с одним методом
type Reader interface {
Read(p []byte) (n int, err error)
}
Лучшие практики и идиомы Go:
Небольшие и сфокусированные интерфейсы: Предпочитайте маленькие интерфейсы (1-3 метода). Это соответствует принципу разделения интерфейсов (Interface Segregation Principle).
io.Reader
,io.Writer
,fmt.Stringer
— идеальные примеры.Именование: Для интерфейсов с одним методом принято добавлять суффикс
-er
к названию метода (Read ->Reader
, Write ->Writer
)."Принимай интерфейсы, возвращай структуры": Это ключевая идиома Go. Ваши функции должны зависеть от абстракций (интерфейсов), а не от конкретных типов. Это делает код более тестируемым и гибким. Возвращать же лучше конкретный тип, чтобы у вызывающего кода был доступ ко всей его функциональности.
Объявление на стороне потребителя: Интерфейс должен определять тот пакет, который его использует, а не тот, который его реализует. Это позволяет избежать циклических зависимостей и лишних связей между пакетами.
Неявная реализация: В Go тип удовлетворяет интерфейсу автоматически, если он реализует все его методы. Ключевое слово
implements
не требуется. Это способствует слабой связности.Пустой интерфейс (
any
):interface{}
(или его псевдонимany
с Go 1.18) может содержать значение любого типа. Используйте его, когда тип данных действительно неизвестен. Для работы с таким значением требуются проверки типа (type assertions).
Пример использования:
// Интерфейс определяет, что нам нужно что-то, что умеет логировать
type Logger interface {
Log(string)
}
// Конкретная реализация
type ConsoleLogger struct{}
func (c ConsoleLogger) Log(msg string) {
fmt.Println(msg)
}
// Функция зависит от интерфейса, а не от ConsoleLogger
func Process(l Logger) {
l.Log("Процесс запущен...")
}
func main() {
// Мы можем передать любую структуру, которая удовлетворяет интерфейсу Logger
cl := ConsoleLogger{}
Process(cl)
}