Зачем нужны интерфейсы

Зачем вообще интерфейсы: развязка кода, подмена реализаций и стандартные контракты Go.

Интерфейсы нужны, чтобы код зависел от поведения, а не от конкретного типа. Это делает программу гибкой и тестируемой.

Развязка через поведение

Представьте функцию, которая шлёт уведомление. Если она зависит от конкретного EmailSender, добавить SMS придётся, переписав функцию. Если же она принимает интерфейс Notifier, любой новый канал подключается без её изменений.

package main

import "fmt"

type Notifier interface {
    Send(msg string) string
}

type Email struct{}
type SMS struct{}

func (Email) Send(msg string) string { return "email: " + msg }
func (SMS) Send(msg string) string   { return "sms: " + msg }

// notify не знает про конкретные каналы — только про контракт
func notify(n Notifier, msg string) {
    fmt.Println(n.Send(msg))
}

func main() {
    notify(Email{}, "привет")
    notify(SMS{}, "привет")
}

Вывод:

email: привет
sms: привет

Тестирование через подмену

Главный практический выигрыш — тесты. Если код зависит от интерфейса, в тесте можно подсунуть фейковую реализацию (mock), не трогая базу или сеть. Это делает Go-код легко тестируемым без специальных фреймворков.

Маленькие интерфейсы — идиома Go

В Go ценят узкие интерфейсы, часто из одного метода. Стандартная библиотека построена на таких: io.Reader (один метод Read), io.Writer (один Write), fmt.Stringer (один String). Их сила в комбинируемости.

package main

import "fmt"

// fmt.Stringer: тип сам решает, как печататься
type Temperature float64

func (t Temperature) String() string {
    return fmt.Sprintf("%.1f°C", float64(t))
}

func main() {
    t := Temperature(23.5)
    fmt.Println(t) // fmt автоматически вызовет String()
}

Вывод:

23.5°C

Реализовав String(), тип получил красивое представление в любом fmt.Println — пакет fmt сам проверяет, удовлетворяет ли значение интерфейсу Stringer.

Правило проектирования

Известная мудрость Go: «принимай интерфейсы, возвращай структуры». Функции должны принимать минимальный интерфейс (чтобы подходило больше типов) и возвращать конкретный тип (чтобы вызывающему было удобнее). И определяйте интерфейс там, где он используется, а не рядом с реализацией.

Итог

  • Интерфейсы развязывают код: функция зависит от поведения, а не от конкретного типа.
  • Это даёт лёгкое тестирование через подмену реализаций (mock).
  • Идиома Go — маленькие интерфейсы (io.Reader, Stringer); «принимай интерфейсы, возвращай структуры».
Проверьте себя
1. Какой главный практический выигрыш интерфейсов?
AУскорение программы
BРазвязка кода и лёгкое тестирование через подмену реализаций
CУменьшение размера бинарника
DАвтоматическая генерация документации
2. Какие интерфейсы предпочитают в Go?
AБольшие, с десятками методов
BМаленькие, часто из одного метода
CТолько пустые
DБез методов вообще
3. Что означает правило «принимай интерфейсы, возвращай структуры»?
AВозвращать только интерфейсы
BФункции принимают минимальный интерфейс, а возвращают конкретный тип
CЗапрет на структуры в аргументах
DИнтерфейсы нельзя возвращать никогда
Поддержать проект