Зачем нужны интерфейсы
Зачем вообще интерфейсы: развязка кода, подмена реализаций и стандартные контракты 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); «принимай интерфейсы, возвращай структуры».