Интерфейсы в Go: упаковка значений
Сейчас (Go 1.19) типы значений интерфейса должны быть базовыми типами интерфейса. Когда упоминается тип значения, он может быть неинтерфейсным типом или базовым интерфейсным типом.
Мы можем рассматривать каждый интерфейс как поле для инкапсуляции значения, не связанного с интерфейсом. Чтобы упаковать/инкапсулировать неинтерфейсное значение в интерфейсное, тип не интерфейсного значения должен реализовывать данный интерфейс.
Если тип T реализует (базовый) тип интерфейса I, то любое значение типа T может быть неявно преобразовано в тип I. Любое значение типа T может быть присвоено (изменяемым) значениям типа I. Когда T значение преобразуется (присваивается) в I значение:
-
Если тип T не является интерфейсным, то копия T значения упаковывается (или инкапсулируется) в результирующее (или целевое) I значение. Временная сложность копии O(n), где n — размер копируемого T значения.
-
Если тип T также является интерфейсным, то копия значения, заключенного в T значение, помещается (или инкапсулируется) в значение результата (или назначения) I. Стандартный компилятор Go выполняет здесь оптимизацию, поэтому временная сложность копии составляет O(1), а не O(n).
Информация о типе упакованного значения также хранится в значении интерфейса результата или назначения. Об этом поговорим далее.
Когда значение заключено в интерфейс, оно называется динамическим значением интерфейса. Тип динамического значения называется динамическим типом интерфейса.
Прямая часть динамического интерфейсного значения является неизменной, хотя мы можем заменить динамическое значение интерфейса другим динамическим значением.
В Go нулевые значения или значения по умолчанию любого интерфейса предварительно равны nil идентификатору. Ничто не заключено в nil интерфейс. Присвоение не типизированного nil интерфейса очистит динамическое значение, заключенное в бокс (контейнер) интерфейса.
Обрати внимание, что нулевые значения многих неинтерфейсных типов в Go также представлены nil в Go, например, слайс или карты (map). Значение nil также упаковывается в интерфейс, но при этом значение не будет равно nil.
Пример:
Самое время протестировать код!
Поскольку любой тип реализует все типы пустых интерфейсов, любое неинтерфейсное значение может быть упаковано или назначено в пустой интерфейс. По этой причине типы пустых интерфейсов можно рассматривать как тип any, имеющийся во многих других языках.
Когда нетипизированное значение (кроме не типизированных nil значений) присваивается пустому интерфейсу, нетипизированное значение будет сначала преобразовано в тип по умолчанию. Мы можем думать, что нетипизированное значение выводится как значение по умолчанию, например не типизированные числовые константы.
Давай рассмотрим пример, который демонстрирует некоторые операции присваивания со значениями интерфейса.
Обрати внимание, что сигнатура функции fmt.Println, много раз использованной в предыдущих разделах, выглядит следующим образом.
Вот почему вызовы функции fmt.Println могут принимать аргументы любых типов, а также любое количество параметров, так как является вариативной функцией.
Ниже приведен еще один пример, показывающий, как пустой интерфейс используется для упаковывания значений любого не интерфейсного типа.
Компиляторы Go создадут глобальную таблицу, содержащую информацию о каждом типе во время компиляции. Обрати внимание, что произойдет оптимизация, интерфейсные значения без явного применения type assertion, будут преобразованы напрямую в значение без упаковки /raw value. Информация включает в себя такие данные, как виды типов, методы и поля, которыми владеет тип, тип элемента, типа контейнера, размеры типа и т. д. Глобальная таблица будет загружена в память при запуске программы.
Во время выполнения, когда значение, не являющееся интерфейсом, будет заключено в интерфейс, среда выполнения Go (по крайней мере, для стандартной среды выполнения Go) будет анализировать и создавать информацию о реализации и сохранять ее в интерфейс. Информация о реализации для каждой пары интерфейсного и неинтерфейсного типа будет построена только один раз и закэширована в глобальной карте для увеличения производительности. Количество записей глобальной карты никогда не уменьшается. На самом деле ненулевой (non-nil) интерфейс просто использует поле внутреннего указателя, которое ссылается на кэшированную запись информации о реализации.
Информация о реализации для каждой пары (тип интерфейса, динамический тип) включает две части информации:
-
Информация динамического типа (неинтерфейсного типа).
-
Таблицу методов (срез), в которой хранятся все соответствующие методы, указанные типом интерфейса и объявленные для неинтерфейсного типа (динамический тип).
Эти две части информации необходимы для реализации двух важных функций в Go:
-
Информация о динамическом типе является ключом к реализации отражения в Go.
-
Информация таблицы методов является ключом к реализации полиморфизма, о котором мы поговорим далее.
Рекомендуем изучить следующие материалы: