Интерфейс это набор методов
Интерфейсный тип перечисляет сигнатуры методов. Любой тип, у которого есть эти методы, удовлетворяет интерфейсу, и никакого объявления об этом не нужно.
Ни Rect, ни Circle не упоминают Shape. Это неявная реализация, определяющая черта интерфейсов в Go. Благодаря ей в своём пакете можно объявить интерфейс, которому уже удовлетворяют типы из других пакетов, не трогая их код.
Маленькие интерфейсы из стандартной библиотеки
Код на Go предпочитает интерфейсы из одного-двух методов. Самые важные:
| Интерфейс | Метод | Где используется |
|---|---|---|
fmt.Stringer | String() string | печать через fmt |
error | Error() string | каждая функция, которая может завершиться ошибкой |
io.Reader | Read(p []byte) (n int, err error) | файлы, сеть, gzip, тела HTTP |
io.Writer | Write(p []byte) (n int, err error) | файлы, буферы, хеши, HTTP-ответы |
sort.Interface | Len, Less, Swap | пакет sort |
http.Handler | ServeHTTP(w, r) | net/http |
Поскольку у io.Reader один метод, его реализуют десятки типов, и любая функция, принимающая io.Reader, работает со всеми ними:
Пословица Go гласит: «чем больше интерфейс, тем слабее абстракция». Большие интерфейсы собирают из маленьких: io.ReadWriter это Reader плюс Writer, записанные встраиванием одного интерфейса в другой.
any: пустой интерфейс
У interface{} нет методов, поэтому ему удовлетворяет любой тип. В Go 1.18 появился псевдоним any; это одно и то же.
Значение any может хранить что угодно, но сделать с ним почти ничего нельзя, пока вы не восстановите конкретный тип через утверждение типа или type switch. Если множество типов известно, лучше настоящий интерфейс или дженерики. any подходит для по-настоящему динамических данных, например декодированного JSON неизвестной структуры, и для печати.
Что содержит значение интерфейса
Значение интерфейса это пара: динамический тип и динамическое значение. var s Shape = Rect{3, 4} хранит тип Rect и копию значения. Вызов s.Area() находит метод Rect во время выполнения.
Интерфейс равен nil, только когда пусты обе части. Из этого правила вырастает самый запутанный баг в Go.
Ловушка nil-интерфейса
nil-указатель, сохранённый в интерфейсе, даёт не-nil интерфейс.
Вывод:
false
*main.MyError true
true
validate(true) возвращает интерфейс error с типом *MyError и значением nil. У интерфейса есть тип, поэтому он не равен nil, и в вызывающем коде срабатывает ветка if err != nil. Вызов err.Error() в ней затем запаникует при обращении к полю nil-получателя.
Исправление простое: объявите переменную как error, а не как конкретный тип-указатель, или возвращайте литерал nil на успешном пути. Никогда не возвращайте конкретный тип-указатель ошибки из функции, результат которой error. Та же ловушка касается любого интерфейса, не только ошибок.
Проверка, что тип реализует интерфейс
Реализация проверяется там, где значение присваивается интерфейсу. Если такого кода пока нет, ошибка в сигнатуре метода останется незамеченной. Пустое присваивание на уровне пакета делает проверку явной:
var _ io.Writer = (*LogWriter)(nil)
var _ fmt.Stringer = Temp(0)
Во время выполнения это ничего не стоит. Если у *LogWriter метод Write(p []byte) error вместо Write(p []byte) (int, error), сборка упадёт:
cannot use (*LogWriter)(nil) (value of type *LogWriter) as io.Writer value in variable declaration: *LogWriter does not implement io.Writer (wrong type for method Write)
have Write([]byte) error
want Write([]byte) (int, error)
Получатели-указатели и интерфейсы
Если у метода получатель-указатель, этот метод есть только у типа-указателя. *Counter удовлетворяет интерфейсу через него, а Counter нет. Компилятор скажет Counter does not implement Incrementer (method Inc has pointer receiver). Сохраняйте в интерфейсе &Counter{}. Наборы методов объясняет страница о методах.
Принимай интерфейсы, возвращай структуры
Распространённое правило в Go: функции принимают параметры-интерфейсы и возвращают конкретные типы.
- Принимая интерфейс, функция позволяет вызывающему коду передать всё, что подходит, включая тестовые заглушки. Функция, которая читает данные, должна принимать
io.Reader, а не*os.File. - Возвращая конкретный тип, функция даёт вызывающему коду все его методы и поля и избегает ловушки nil-интерфейса.
os.Openвозвращает*os.File, а неio.Reader.
Связанная привычка: объявлять интерфейсы там, где они используются, а не там, где реализуются. Если вашему сервису нужно что-то, что умеет Get(id) пользователя, объявите интерфейс из одного метода в пакете сервиса, а пакет базы данных пусть просто экспортирует свою структуру.
Сравнение значений интерфейсов
Два значения интерфейса равны, когда их динамические типы идентичны, а динамические значения равны. Если динамический тип несравним (слайс, мапа), == компилируется, но паникует во время выполнения: runtime error: comparing uncomparable type []int.
Частые ошибки
- Возврат типизированного nil-указателя в виде интерфейса. Возвращайте литерал
nil. - Интерфейсы раньше времени. Сначала напишите конкретный тип. Добавьте интерфейс, когда понадобится вторая реализация или тест.
- Указатель на интерфейс.
*io.Readerпочти никогда не нужен. Интерфейс и так хранит указатель, если вы положили в него указатель. - Большие интерфейсы. Интерфейсы из десяти методов трудно реализовать и трудно подменить заглушкой. Разбивайте их.
Часто задаваемые вопросы
Как реализовать интерфейс в Go?
Определите на своём типе методы, перечисленные в интерфейсе, с теми же именами и сигнатурами. Ключевого слова implements нет. Если у *File есть Read(p []byte) (int, error), он автоматически является io.Reader. Компилятор проверяет это везде, где вы присваиваете значение переменной интерфейсного типа.
Что такое пустой интерфейс или any в Go?
У interface{} нет методов, поэтому ему удовлетворяет любой тип. Начиная с Go 1.18 any это встроенный псевдоним interface{}. Значение типа any может хранить что угодно, но чтобы достать конкретный тип обратно, нужно утверждение типа или type switch.
Почему интерфейс в Go не nil, хотя я присвоил nil-указатель?
Значение интерфейса хранит тип и значение. Если присвоить nil *MyError переменной error, получится интерфейс с типом *MyError и значением nil, и такой интерфейс не равен nil. Когда ошибки нет, возвращайте литерал nil, а не типизированный nil-указатель.
Как проверить на этапе компиляции, что тип реализует интерфейс?
Добавьте пустое присваивание на уровне пакета: var _ io.Reader = (*MyReader)(nil). Если у *MyReader не хватает метода, сборка упадёт с сообщением, где назван недостающий метод.