В предыдущих уроках в определениях функций и структур использовались интерфейсы. Но бывают ситуации, когда не всегда можно указать конкретные типы переменных. Например, если получаем JSON-документ от пользователя, то структура этого документа может быть любой.
Разрешить функции принимать произвольный параметр можно с помощью пустого интерфейса — интерфейса без методов.
Действительно, так как интерфейс является набором требований к типу, то пустой интерфейс ничего от типа не требует. Он может быть чем угодно. Однако и использовать переменную типа пустого интерфейса невозможно.
Note
В Go 1.18 у типа
interface{}
появился более короткий и понятный псевдонимany
(от англ. any — любой).
Но с переменной v
нельзя ничего сделать. Нужно привести её к определённому типу оператором v.(Тип)
.
Оператор приведения типа приводит переменную интерфейсного типа к конкретному типу или другому интерфейсу.
Второй вариант использования более предпочтителен.
Оператором switch
можно лаконично запрограммировать логику относительно каждой проверки типа. Для примера напишем свою реализацию функции fmt.Printf
:
Пример: нужно реализовать обобщение операции умножения для чисел и строк. Если первый аргумент функции — строка, то повторить её b
раз, а если число, то вернуть a*b
.
Внутреннее устройство интерфейсов
Чтобы понять, каким образом интерфейсы приводятся к другим типам, следует разобраться, как устроен интерфейс изнутри.
Соединим примеры из предыдущего и текущего уроков:
Вот какие данные будут храниться в переменной v Stringer
:
То, что изображено на схеме, можно посмотреть подробнее в src/runtime/runtime2.go.
На схеме видно, что интерфейс состоит из двух указателей: на метаданные типа и на сами данные. При приведении типа используются эти метаданные, чтобы вычислить, какой конкретный тип представляет этот интерфейс и как правильно разыменовать указатель на данные.
Почему так устроено? Всё дело в том, что переменной интерфейсного типа могут быть присвоены данные разного размера. Например, интерфейсу Stringer
может удовлетворять и большая сложная структура, и пользовательский int
. Поэтому сохраняем данные по указателю. А вот в itable
храним метаинформацию о типе, который там содержится.
Когда присваиваем переменную конкретного типа, происходит следующее:
Note
Интерфейсная переменная по природе относится к ссылочному типу. Если передать её в функцию, то она скопируется, но, так как указатели будут указывать на ту же исходную переменную, изменение переменной через вызов методов может изменить данные.
В Go есть особенность, связанная с nil
. Посмотрите ещё раз на схему: интерфейс может быть nil
, а может быть с nil
-указателем на данные. Покажем пример, где эта особенность приводит к ошибке:
Код выведет строчку "error is not nil"
, потому что Go обернёт nil
-указатель *MyError
в не-nil
-интерфейс Error
.
То есть мы присвоили переменной типа interface{}
переменную определённого типа. Это значит, что метаданные непустые. Исправить проблему можно так:
В этом случае код выведет строчку "error is nil"
, потому что сам интерфейс Error
будет nil
.
Сравнение интерфейсов
Сравнение интерфейсных типов имеет одну особенность, которую важно знать, так как она может привести к неочевидным ошибкам. Рассмотрим следующий пример:
На первый взгляд, в программе творится нечто странное: с одной стороны, мы сравнили без ошибок мапу и целое, с другой — получили ошибку при сравнении переменной с самой собой.
Всё дело в том, что интерфейсы сравниваются по цепочке. Сначала сравнивается тип: если типы переменных внутри разные, то вернётся false; если одинаковые, то сравниваются уже сами данные. А map
сравнивать между собой нельзя, и получаем панику.
Ключевые мысли
- Интерфейсные переменные по своей природе динамичны. Их можно преобразовать к переменным другого типа через операцию приведения типа
a.(type)
. - Интерфейс изнутри представляет собой два указателя — на обёрнутую переменную и на информацию о её типе. Преобразование типа изменяет информацию о типе либо возвращает обёрнутую переменную.
- Операция приведения типа — это не то же самое, что преобразование типа.
- Интерфейс, равный
nil
, и интерфейс, оборачивающийnil
, — разные вещи. - Сравнение интерфейсов необходимо делать аккуратно.
- Наилучшим вариантом использования интерфейса является всё же использование не пустого, а конкретного интерфейса и обращение к его методам. Однако иногда без них не обойтись.
📂 Go | Последнее изменение: 28.08.2024 21:02