Go. Структуры данных
Интерфейсы: объявление
и реализация, полиморфизм

Введение
Представьте, что вы разрабатываете систему уведомлений. Пользователи могут получать уведомления по email, SMS, push-уведомлениям или в Telegram. Каждый способ отправки имеет свою специфику, но с точки зрения бизнес-логики все они делают одно — отправляют сообщение.
Как написать код, который работает со всеми этими способами одинаково, не привязываясь к конкретной реализации? Ответ — интерфейсы.
Интерфейсы в Go — это один из ключевых инструментов для создания гибкого, расширяемого кода. Они позволяют описать поведение объекта, не заботясь о том, как именно это поведение реализовано. Это основа полиморфизма в Go и главный способ достижения слабой связанности между компонентами системы.
В этом лонгриде мы разберём, как объявлять интерфейсы, как Go автоматически определяет их реализацию, и как использовать полиморфизм для написания универсального кода.
Содержание
Что такое интерфейсы?
Интерфейс в Go — это тип, который описывает набор методов. Интерфейс не говорит, как что-то должно быть сделано, он говорит только что должно быть сделано.
Давайте вернёмся к примеру с уведомлениями. Нам не важно, как именно работает отправка через email или SMS — важно, что оба способа умеют отправлять сообщение. Это и есть интерфейс: контракт, который говорит «любой тип, который умеет отправлять сообщения, может быть использован здесь».
В Go интерфейсы реализуются неявно. Это означает, что вам не нужно объявлять, что ваш тип реализует какой-то интерфейс. Если тип имеет все методы, которые требует интерфейс, то он автоматически этот интерфейс реализует.
Объявление интерфейсов
Базовый синтаксис
Интерфейс объявляется с помощью ключевого слова type и содержит список сигнатур методов:
type Notifier interface {
    Send(message string) error
}
Здесь мы объявили интерфейс Notifier, который требует наличия метода Send с параметром message типа string и возвращаемым значением типа error.
Любой тип, у которого есть метод с такой же сигнатурой, будет реализовывать этот интерфейс автоматически.
Соглашения об именовании
В Go принято называть интерфейсы с одним методом, добавляя суффикс -er к названию метода:
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}
Такое соглашение делает код более читаемым: вы сразу понимаете, что Reader — это что-то, что умеет читать, Writer — что умеет писать, а Closer — что умеет закрываться.
Для интерфейсов с несколькими методами используются описательные имена:
type FileSystem interface {
    Open(name string) (File, error)
    Remove(name string) error
    Stat(name string) (FileInfo, error)
}
Реализация интерфейсов
Неявная реализация
Главная особенность интерфейсов в Go — неявная реализация. Вам не нужно явно указывать, что тип реализует интерфейс. Достаточно просто определить нужные методы.
Вернёмся к нашему примеру с уведомлениями:
type Notifier interface {
    Send(message string) error
}

// EmailNotifier отправляет уведомления по email.
type EmailNotifier struct {
    from string
}

// Send отправляет email.
func (e EmailNotifier) Send(message string) error {
    fmt.Printf("Sending email from %s: %s\n", e.from, message)
    return nil
}
Обратите внимание: мы нигде не написали, что EmailNotifier реализует интерфейс Notifier. Но поскольку у типа есть метод Send с нужной сигнатурой, он автоматически реализует Notifier.
Теперь мы можем использовать EmailNotifier там, где ожидается Notifier:
func notify(n Notifier, message string) {
    err := n.Send(message)
    if err != nil {
        fmt.Printf("Failed to send: %v\n", err)
    }
}

func main() {
    email := EmailNotifier{from: "admin@example.com"}
    notify(email, "Hello via email!")
}
Добавим второй тип:
// SMSNotifier отправляет уведомления по SMS.
type SMSNotifier struct {
    phoneNumber string
}

// Send отправляет SMS.
func (s SMSNotifier) Send(message string) error {
    fmt.Printf("Sending SMS to %s: %s\n", s.phoneNumber, message)
    return nil
}
SMSNotifier тоже реализует Notifier, и функция notify работает с ним точно так же:
sms := SMSNotifier{phoneNumber: "+1234567890"}
notify(sms, "Hello via SMS!")
Мощь интерфейсов в том, что код, работающий с интерфейсом, не знает и не должен знать, с каким конкретным типом он работает. Вы можете добавлять новые реализации (например, PushNotifier или TelegramNotifier) без изменения существующего кода — достаточно реализовать метод Send.
Pointer receiver vs value receiver
Важный момент: имеет значение, как определён receiver метода.
Если метод использует pointer receiver, только указатель на тип реализует интерфейс:
type Counter struct {
    count int
}

func (c *Counter) Increment() {  // ← pointer receiver
    c.count++
}

type Incrementer interface {
    Increment()
}

var inc Incrementer
c := Counter{}
inc = &c  // ✅ Работает, *Counter реализует Incrementer
// inc = c  // ❌ Ошибка: Counter не реализует Incrementer
Причина: методы с pointer receiver могут изменять объект. Если бы Go разрешил присвоить значение интерфейсу, изменения внутри метода были бы потеряны при копировании.
Если метод использует value receiver, интерфейс реализуют и тип, и указатель на тип:
func (r Rectangle) Area() float64 {  // ← value receiver
    return r.Width * r.Height
}

rect := Rectangle{Width: 10, Height: 5}
var s Shape
s = rect   // ✅ Работает
s = &rect  // ✅ Тоже работает
Go автоматически разыменовывает указатель при вызове метода с value receiver.
Если хотя бы один метод интерфейса использует pointer receiver, только указатель на тип будет реализовывать интерфейс.
Полиморфизм в Go
Что такое полиморфизм?
Полиморфизм — это способность обрабатывать объекты разных типов через единый интерфейс. В Go полиморфизм достигается через интерфейсы: один и тот же код может работать с разными типами, если они реализуют нужный интерфейс.
В отличие от языков с классическим ООП (Java, C++), в Go нет наследования классов. Вместо этого используется композиция и интерфейсы для достижения полиморфного поведения.
Полиморфизм через интерфейсы
Рассмотрим пример с геометрическими фигурами:
type Shape interface {
    Area() float64
    Perimeter() float64
}

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}
Теперь у нас есть два разных типа — Rectangle и Circle, но оба реализуют интерфейс Shape. Мы можем написать функцию, которая работает с любой фигурой:
func printShapeInfo(s Shape) {
    fmt.Printf("Area: %.2f\n", s.Area())
    fmt.Printf("Perimeter: %.2f\n", s.Perimeter())
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    circle := Circle{Radius: 7}
    
    printShapeInfo(rect)    // ← Работает с прямоугольником
    printShapeInfo(circle)  // ← Работает с кругом
}
Функция printShapeInfo полиморфна: она работает с любым типом, реализующим Shape, не зная его конкретного типа.
Интерфейсы как параметры функций
Использование интерфейсов в качестве параметров функций — ключевая практика для написания гибкого кода. Вместо того чтобы привязываться к конкретному типу, мы требуем только наличие нужного поведения:
func calculateTotalArea(shapes []Shape) float64 {
    total := 0.0
    for _, shape := range shapes {
        total += shape.Area()
    }
    return total
}

func main() {
    shapes := []Shape{
        Rectangle{Width: 10, Height: 5},
        Circle{Radius: 7},
        Rectangle{Width: 3, Height: 4},
    }
    
    total := calculateTotalArea(shapes)
    fmt.Printf("Total area: %.2f\n", total)
}
Такой подход позволяет легко расширять функциональность: если мы добавим новый тип фигуры (например, треугольник), достаточно реализовать для него методы Area() и Perimeter(), и он автоматически заработает со всеми существующими функциями.
Пустой интерфейс
interface{} и any
Пустой интерфейс не содержит ни одного метода. Поскольку любой тип имеет как минимум ноль методов, любой тип реализует пустой интерфейс:
var i interface{}

i = 42           // ← int
i = "hello"      // ← string
i = true         // ← bool
i = []int{1, 2}  // ← slice
Начиная с Go 1.18, был введён псевдоним any, который является более читаемой альтернативой interface{}:
var i any

i = 42
i = "hello"
Оба варианта идентичны, но any более явно выражает намерение: «это может быть что угодно».
Когда использовать пустой интерфейс
Пустой интерфейс полезен, когда нужно работать с данными неизвестного типа. Например, в функциях для работы с JSON или в универсальных контейнерах:
func printAny(value any) {
    fmt.Printf("Value: %v, Type: %T\n", value, value)
}

func main() {
    printAny(42)
    printAny("hello")
    printAny([]int{1, 2, 3})
}
Однако использование any снижает типобезопасность. Компилятор не может проверить, что вы делаете с этим значением, и вам придётся использовать type assertions для получения конкретного типа.
Используйте any только когда это действительно необходимо. Там, где возможно, предпочитайте конкретные интерфейсы или дженерики (начиная с Go 1.18).
Проверка типов
Type assertion
Type assertion позволяет извлечь конкретное значение из интерфейса. Это нужно, когда вы знаете (или предполагаете), что интерфейс содержит значение определённого типа:
var i any = "hello"

s := i.(string)      // ← Извлекаем string
fmt.Println(s)       // "hello"
Если тип не совпадает, произойдёт паника:
var i any = 42
s := i.(string)  // panic: interface conversion: interface {} is int, not string
Чтобы избежать паники, используйте форму с двумя возвращаемыми значениями:
var i any = 42

s, ok := i.(string)
if ok {
    fmt.Println("It's a string:", s)
} else {
    fmt.Println("Not a string")  // ← Выполнится этот блок
}
Второе значение ok показывает, успешно ли прошло преобразование.
Type switch
Когда нужно обработать несколько возможных типов, удобнее использовать type switch:
func describe(i any) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Integer: %d\n", v)  // ← v имеет тип int
    case string:
        fmt.Printf("String: %s\n", v)  // ← v имеет тип string
    case bool:
        fmt.Printf("Boolean: %t\n", v)
    default:
        fmt.Printf("Unknown type: %T\n", v)
    }
}

func main() {
    describe(42)
    describe("hello")
    describe(true)
    describe(3.14)
}
Type switch более читаем и удобен, чем цепочка if-else с type assertions. Обратите внимание на специальный синтаксис i.(type) — он работает только внутри switch.
В одном case можно указать несколько типов, но тогда переменная будет иметь тип any:
func process(i any) {
    switch v := i.(type) {
    case int, int64:
        // v имеет тип any, нужен type assertion для использования
        fmt.Printf("Integer type: %T\n", v)
    case string:
        // v имеет тип string
        fmt.Printf("String: %s\n", v)
    }
}
Композиция интерфейсов
Интерфейсы в Go можно комбинировать, встраивая один интерфейс в другой. Это создаёт новый интерфейс, который требует реализации всех методов встроенных интерфейсов:
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type ReadWriter interface {
    Reader  // ← Встраивание интерфейса Reader
    Writer  // ← Встраивание интерфейса Writer
}
Тип, реализующий ReadWriter, должен иметь оба метода: Read и Write.
Стандартная библиотека Go активно использует композицию интерфейсов:
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}
Такой подход позволяет строить сложные интерфейсы из простых, переиспользуя базовые контракты.
Заключение
Интерфейсы — это сердце гибкого дизайна в Go. Они позволяют писать код, который работает с поведением, а не с конкретными типами, делая системы расширяемыми и легко тестируемыми.
Ключевые моменты:
  • В Go интерфейсы реализуются неявно — достаточно просто определить нужные методы
  • Интерфейсы позволяют писать функции и структуры данных, работающие с разными типами через единый контракт
  • Чем меньше методов в интерфейсе, тем проще его реализовать и тем больше типов смогут его удовлетворить
  • Методы с pointer receiver могут реализовать интерфейс только через указатель, с value receiver — и через указатель, и через значение
  • Пустой интерфейс (any) может содержать значение любого типа, но снижает типобезопасность
  • Type assertions и type switches позволяют извлекать конкретные типы из интерфейсов
  • Сложные интерфейсы строятся из простых через встраивание
Что дальше?
Теперь вы готовы заглянуть внутрь интерфейсов:
  • Интерфейсы: внутреннее устройство
Интерфейсы — это один из самых мощных инструментов Go. Используйте их с умом, и ваш код станет гибким и элегантным!