Интерфейсы и неявная реализация
На собеседовании спросят: «Как Go понимает, что тип реализует интерфейс, если нигде не написано implements?»
Duck typing (утиная типизация) — если объект умеет делать то, что требует интерфейс, он ему подходит. Никаких деклараций не нужно: «если крякает как утка — значит утка».
Что выведет этот код?
Начнём с примера. Вот интерфейс и тип, который его реализует:
type Stringer interface {
String() string
}
type Cat struct {
Name string
}
func (c Cat) String() string {
return "Кот по имени " + c.Name
}
func Describe(s Stringer) {
fmt.Println(s.String())
}
func main() {
c := Cat{Name: "Барсик"}
Describe(c)
}Вывод:
Кот по имени БарсикОбратите внимание: нигде не написано Cat implements Stringer или Cat : Stringer. Мы просто определили у структуры Cat метод String() string — ровно такой, какой требует интерфейс Stringer. И этого достаточно, чтобы передать Cat в функцию Describe, которая принимает интерфейс.
Как это работает под капотом
В языках вроде Java или C# принадлежность к интерфейсу — это явное решение, зафиксированное в объявлении класса: class Cat implements Stringer. Компилятор смотрит на эту декларацию и проверяет её.
В Go всё наоборот. Компилятор не смотрит на декларации — он смотрит на набор методов у типа. Если у типа T есть все методы, перечисленные в интерфейсе I (с точно такой же сигнатурой), то значение типа T можно использовать там, где ожидается I. Это называется структурной типизацией: важна не «родословная» типа, а его форма — какие у него есть методы.
Отсюда и разговорное название — duck typing: «если оно крякает как утка и плавает как утка, то это утка», даже если нигде не написано «это утка». Проверка происходит на этапе компиляции, поэтому ошибку вы увидите сразу, а не во время выполнения программы, как в Python.
Есть удобный приём, которым пользуются на практике, чтобы явно зафиксировать «этот тип обязан реализовывать вот этот интерфейс» — даже если сейчас нигде в коде эта связь не используется:
var _ Stringer = Cat{}Эта строка ничего не делает во время выполнения — переменная _ отбрасывается. Но если в какой-то момент вы уберёте у Cat метод String() или измените его сигнатуру, компиляция сломается прямо на этой строке с понятной ошибкой. Это своего рода тест на совместимость, встроенный в компилятор.
Как проверить, реализует ли тип интерфейс
Есть три способа понять, подходит ли тип под интерфейс:
- Прочитать методы типа. Откройте объявление структуры и посмотрите, какие методы у неё определены — сравните с методами интерфейса.
- Дать компилятору проверить это за вас — приём с
var _ Interface = Type{}, показанный выше. - Проверить в рантайме через type assertion — если у вас уже есть значение интерфейсного типа, а не конкретного, можно спросить: «а этот конкретный объект реализует вот такой более узкий интерфейс?» — но об этом подробнее в следующих уроках раздела.
Главная ловушка: nil внутри интерфейса — это не nil
А теперь тот самый вопрос, который любят задавать на собеседованиях именно по этой теме. Что выведет вот такой код?
type Animal interface {
Sound() string
}
type Dog struct{}
func (d *Dog) Sound() string {
if d == nil {
return "..."
}
return "Гав!"
}
func GetAnimal() Animal {
var d *Dog // d == nil, но это указатель конкретного типа *Dog
return d
}
func main() {
a := GetAnimal()
if a == nil {
fmt.Println("Животного нет")
} else {
fmt.Println("Животное есть:", a.Sound())
}
}Вывод:
Животное есть: ...Большинство людей интуитивно ожидают увидеть «Животного нет» — ведь d действительно равен nil. Но проверка a == nil возвращает false. Почему?
Дело в том, что интерфейсное значение в Go — это на самом деле пара из двух частей: (тип, значение). Когда мы возвращаем d (указатель типа *Dog, равный nil) как значение интерфейса Animal, пара получается такой: (*Dog, nil). Тип не пустой — он равен *Dog! А интерфейс равен nil только тогда, когда пуста ОБЕ части пары: и тип, и значение. Поэтому a == nil — ложь: у интерфейса есть конкретный тип, просто внутри лежит нулевое значение этого типа.
Частые ошибки на собеседовании
- Путают структурную и номинативную типизацию. Говорят «Cat implements Stringer», хотя в Go такого синтаксиса нет вообще — реализация выводится автоматически по набору методов.
- Не понимают ловушку nil-интерфейса. Функция, которая возвращает интерфейс с типизированным nil-указателем внутри, — частая причина трудноуловимых багов в реальном коде, особенно при обработке ошибок через
error. - Забывают, что методы с получателем-указателем (
*Dog) и получателем-значением (Dog) реализуют интерфейс по-разному. Если метод определён на*Dog, то интерфейсу удовлетворяет именно*Dog, а неDog— это отдельная частая путаница, к которой мы ещё вернёмся.
Итоги
- В Go нет ключевого слова implements — принадлежность типа интерфейсу определяется автоматически по набору методов (структурная типизация, «duck typing»).
- Приём
var _ Interface = Type{}— способ явно зафиксировать соответствие и поймать несоответствие ещё на этапе компиляции. - Значение интерфейса — это пара (тип, значение). Интерфейс равен nil, только если nil обе части пары — отсюда классическая ловушка с типизированным nil-указателем внутри интерфейса.