Приключение с interface в мире Golang
Задумывались что такое interface? Как выглядит его проинстанцированный объект? Каким свойством обладает при сравнении с nil? Сейчас расскажу.
6К открытий6К показов
Евгений Митин
Руководитель отдела разработки платформенных сервисов
Вы когда-нибудь задумывались что такое interface
? Ну, то есть, не ключевое слово синтаксиса, а что это такое в рантайме? Как выглядит его проинстанцированный объект? А, главное, каким свойством обладает при сравнении с nil
? Нет? Тогда устраивайтесь поудобнее, я сейчас вам всё расскажу.
Начнём с определения интерфейса из спецификации языка:
An interface type specifies a method set called its interface. A variable of interface type can store a value of any type with a method set that is any superset of the interface. Such a type is said to implement the interface. The value of an uninitialized variable of interface type is nil.
На мой взгляд, хорошее абстрактное описание, которое не очень сильно помогает осознать интерфейсы на практике, но в целом даёт общее понимание. Очень советую пойти в документации в раздел Interface types — там много примеров и более подробное описание.
Но этого поста бы не было, будь там вся необходимая информация ? Допустим, мы уже понимаем, что переменная с интерфейсом в качестве типа это некий объект, который с точки зрения типизации обязан обладать всеми перечисленными в указанном интерфейсе методами.
Необходимо также понимать, что переменные с интерфейсом в качестве типа бессмысленны сами по себе, они не создаются и не используются в отрыве от обычных типов. И именно переменные обычных типов, превращаются в переменные интерфейсного типа. А с точки зрения компилятора, в переменную интерфейсного типа, может превратится любая переменная обычного типа, у которой её тип содержит все заявленные в интерфейсе методы. Хотя могут быть и дополнительные методы — реализация интерфейса от этого не сломается.
Но что происходит, например, когда мы переменную определённого, конкретного типа, превращаем в другую переменную с интерфейсным типом? В Golang, как впрочем и многих других статически типизированных языках, у переменной можно получить доступ только к тем атрибутам и методам, которые существуют у типа этой переменной. В случае преобразования переменная получает определенный интерфейсный тип. Одна из очевидных проблем, которую мы встречаем, — потеря доступа ко всем атрибутам нашего конкретного типа и к его методам, которые не были указаны в интерфейсе.
Посмотрим на конкретном примере:
Что же происходит, и почему пакет reflect
определяет тип нашего воркера как Man
, которым, на самом деле, он не является? Чтобы это понять, нужно погрузиться в недра исходного кода нашего «суслика» и посмотреть, чем представлен там тип interface
:
Если не вдаваться в подробности, то в поле tab
у нас хранится информация о конкретном типе объекта, который был преобразован в интерфейс. А в поле data
— ссылка на реальную область памяти, в которой лежат данные изначального объекта, в нашем случае Василия. Поэтому библиотека рефлект, когда хочет получить настоящий исходный тип объекта интерфейса, идёт в это поле tab
и получает информацию о типе там.
У нас остаётся возможность преобразовать наш объект интерфейса обратно к исходному типу, для этого у нас есть синтаксис утверждений типа, выглядит он следующим образом:
Можно использовать и без переменной ok
, но в таком случае Вы получите панику, если в интерфейсе окажется несоответствующий тип.
Окей, с этим вроде разобрались, идём дальше.
Как нам говорит документация: The value of an uninitialized variable of interface type is nil. То есть наша переменная интерфейсного типа может принимать nil
. Ну это же супер! Если отмотать немного назад, то можно увидеть, как мы изначально инициализировали нашего работника — var worker Programmer
. Если верить документации, то в этот момент наш worker
был равен nil
. Доверяй, но проверяй:
Но мы пойдём дальше: а что если попробовать создать пустую переменную типа *Man
и преобразовать её к интерфейсу? Пробуем:
Давайте разбираться, почему так происходит. Как я уже говорил, объект интерфейса в Golang содержит два поля: tab
с информацией о конкретном типе и data
, где лежит ссылка на сами данные. И вот, по правилам Golang, интерфейс может быть равен nil
только если оба этих поля не определены.
Давайте смотреть, что у нас получается, когда мы преобразуем переменную man
в интерфейс:
Как итог, мы имеем интерфейс с заполненным полем tab
, и проверка на равенство с nil
всегда будет возвращать false
. Несмотря на то, что изначально переменная возвращала true
при сравнении с nil
.
Описано ли это поведение в документации к нашему суслику? — Нет. Ну точнее не совсем. В спецификации языка это не описано, но это есть в FAQ по языку: Why is my nil error value not equal to nil?. И там как раз рассмотрена одна из распространённых ошибок, связанная с этим поведением интерфейсов: когда мы из функции возвращаем ссылку на объект нашей кастомной ошибки.
Данная ссылка, в свою очередь, может быть nil
, если ошибка в функции не произошла. И, когда происходит преобразование нашего объекта кастомной ошибки к интерфейсу error
, этот новый преобразованный объект уже никогда больше не будет равен nil
, и все проверки if err != nil {}
перестанут работать корректно.
6К открытий6К показов