Первый очерк из цикла приключений в мире сусликов. С этой статьи начинается серия небольших рассказов о необычных подводных камнях, которые можно встретить в начале разработки на Go. В статьях будут примеры кода, будьте с ними аккуратнее - не все из них будут компилироваться и работать, читайте внимательно комментарии, везде будет указано, на какой строке происходит ошибка. Также в блоках кода везде табуляция заменена на пробелы - это сделано намеренно, чтобы статьи выглядели у всех одинаково. Начинать рассказывать я буду издалека, с самых основ, но это необходимо для полного понимания. В конце же рассказа, будет самое интересное, но всё равно стоит читать его с самого начала. Пара слов обо мне. Я всю жизнь занимаюсь разработкой программного обеспечения, в основном в сфере WEB, успел познакомиться с многими языками программирования и поработать в разных крупных компаниях. Сейчас руковожу разработкой в компании NUT.Tech, мы там делаем классные и интересные вещи. В данный момент в основном разработка в отделе построена вокруг Go, поэтому о нём я вам и расскажу. Статьи серии: Интерфейсы в Go - как красиво выстрелить себе в ногу Нарезаем массивы правильно в Go ... Пожалуй, начнём. Вы когда-нибудь задумывались, что такое 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. Мой вольный перевод: Тип интерфейса определяет множество методов, называющееся интерфейс. Переменная типа интерфейса может хранить значение любого типа с набором методов, который является любым надмножеством интерфейса. Говорят, что такой тип реализует интерфейс. Значение неинициализированной переменной типа интерфейса равно nil. На мой взгляд, хорошее абстрактное описание, которое не очень сильно помогает осознать интерфейсы на практике, но в целом даёт общее понимание. Очень советую пойти в документации в раздел Interface_types - там много примеров и более подробное описание. Но этого поста бы не было, будь там вся необходимая информация ;) Допустим, мы уже понимаем, что переменная с интерфейсом в качестве типа это некий объект, который с точки зрения типизации обязан обладать всеми перечисленными в указанном интерфейсе методами. Необходимо также понимать, что переменные с интерфейсом в качестве типа бессмысленны сами по себе, они не создаются и не используются в отрыве от обычных типов. И именно переменные обычных типов, превращаются в переменные интерфейсного типа. А с точки зрения компилятора в переменную интерфейсного типа может превратится любая переменная обычного типа, у которой её тип содержит все заявленные в интерфейсе методы. Хотя могут быть и дополнительные методы - реализация интерфейса от этого не сломается. Понимаю, всё несколько сложно и запутано, но дальше будут примеры и станет понятнее. А что происходит, например, когда мы переменную определенного, конкретного типа, превращаем в другую переменную с интерфейсным типом? В Golang, как впрочем и многих других статически типизированных языках, у переменной можно легально получить доступ только к тем атрибутам и методам, которые существуют у типа этой переменной. В случае преобразования переменная получает определенный интерфейсный тип. Одна из очевидных проблем, которую мы встречаем, - потеря доступа ко всем атрибутам нашего конкретного типа и к его методам, которые не были указаны в интерфейсе. Посмотрим на конкретном примере: Что же происходит, и почему пакет reflect определяет тип нашего воркера как Man, которым на самом деле он не является? Чтобы это понять, нужно погрузиться в недра исходного кода нашего "суслика" и посмотреть, чем представлен там тип interface: Если не вдаваться в подробности, то в поле tab у нас хранится информация о конкретном типе объекта, который был преобразован в интерфейс. А в поле data - ссылка на реальную область памяти, в которой лежат данные изначального объекта, в нашем случае Василия. Поэтому библиотека reflect, когда хочет получить настоящий исходный тип объекта интерфейса, идёт в это поле tab и получает информацию о типе там. У нас остаётся возможность преобразовать наш объект интерфейса обратно к исходному типу, для этого у нас есть синтаксис утверждений типа, выглядит он следующим образом: Можно использовать и без переменной ok, но в таком случае Вы получите панику, если в интерфейсе окажется несоответствующий тип. Окей, с этим вроде разобрались, идём дальше. Как нам говорит документация: The value of an uninitialized variable of interface type is nil. То есть наша переменная интерфейсного типа может принимать nil. Ну это же супер! Если отмотать немного назад, то можно увидеть, как мы изначально инициализировали нашего работника - var worker Programmer. Если верить документации, то в этот момент наш worker был равен nil. Доверяй, но проверяй: Но мы пойдём дальше: а что если попробовать создать пустую переменную типа *Man и преобразовать её к интерфейсу? Пробуем: Давайте разбираться, почему так происходит. Как я уже говорил, объект интерфейса в Go содержит два поля: tab с информацией о конкретном типе и data, где лежит ссылка на сами данные. И вот, по правилам Go, интерфейс может быть равен nil только если оба этих поля не определены. Давайте смотреть, что у нас получается, когда мы преобразуем переменную man в интерфейс: Как итог, мы имеем интерфейс с заполненным полем tab, и проверка на равенство с nil всегда будет возвращать false. Несмотря на то, что изначально переменная возвращала true при сравнении с nil. Описано ли это поведение в документации к нашему суслику? - Нет. Ну точнее не совсем. В спецификации языка это не описано, но это есть в FAQ по языку: https://golang.org/doc/faq#nil_error. И там как раз рассмотрена одна из распространённых ошибок, связанная с этим поведением интерфейсов: когда мы из функции возвращаем ссылку на объект нашей кастомной ошибки. Данная ссылка, в свою очередь, может быть nil, если ошибка в функции не произошла. И, когда происходит преобразование нашего объекта кастомной ошибки к интерфейсу error, этот новый преобразованный объект уже никогда больше не будет равен nil, и все проверки if err != nil {} перестанут работать корректно. Приведу пример чуть более подробный чем в faq: На этом мой очерк заканчивается, читайте о других интересных моментах из мира Go в следующих сериях и будьте аккуратны с интерфейсами. Freelance sommelier