Отражения Go - это тема, которая требует понимания внутреннего устройства go, касающегося структур, интерфейсов и системы типов, чтобы понять, как это в основном работает за кулисами.

Разумеется, вы можете использовать отражения, не вдаваясь в подробности. Цель этой статьи - познакомить вас с некоторыми деталями таким образом, чтобы облегчить понимание. Но это не обязательно.

В этой статье предполагается, что вы имеете базовое представление о структурах и интерфейсах.
Вы можете быстро освежить в памяти Go by example для struct и interfaces или углубиться в обзор Go для Structs и интерфейсы .

Что такое отражение?

В информатике рефлексия - это способность процесса исследовать, анализировать и изменять свою собственную структуру и поведение. - Википедия

Отражения - это манипуляции с программой во время выполнения. Это форма метапрограммирования, но не все, что считается метапрограммированием, является отражением.

Почему это важно для Go?

Есть много аспектов, в которых отражения имеют значение. В этом руководстве я сосредоточусь на наиболее очевидном:
Go - это язык со статической типизацией, вы должны заранее объявить все свои типы. Таким образом, у вас нет способа обрабатывать типы, которые вы не знаете заранее, даже когда манипуляции, исследование или самоанализ, которые вам нужно сделать с ними, не требуют этих знаний заранее.

Хорошим примером является print метод fmt. Если вы хотите напечатать тип переменной с помощью %T, пакету fmt не нужно знать о вашем собственном созданном struct, например, с именем Person. Но он все еще может распечатать Person.

Пустой интерфейс

Интерфейс - это тип с набором методов, причем каждая структура, реализующая эти методы, автоматически реализует интерфейс. Это позволяет использовать интерфейс как тип для методов, которые вы можете передать в структурах, в которых он реализован.

Тогда для пустого интерфейса каждая структура и каждый «примитивный» тип по своей сути реализует его.

Следовательно, использование пустого интерфейса в качестве типа позволяет передать на его место любой тип. Это похоже на использование типа any, но не совсем.

Приведенный выше код будет работать и будет печатать 101 для первой печати, поскольку он использует + для x, который набирается int, а затем переходит к myPrint, он отправляет его Println, который также принимает пустые значения интерфейса и использует внутренние отражения.

Но Go по-прежнему является языком со статической типизацией, поэтому использование пустого интерфейса не позволит вам ничего делать с переменной (если вы не используете утверждения типа или отражения).

Приведенный выше код не компилируется. внутри myPrint тип item - это пустой интерфейс, поэтому, хотя базовый тип является целым числом, go еще не «знает» об этом и поэтому запаникует.

Утверждения типа

Утверждения типа - это утилита, которая помогает вам проверить базовый тип для некоторой переменной, и в случае, если это тот тип, который вы утверждали, получить базовую переменную значения, распознанную как этот тип.

Это напечатает число 10, поскольку мы сначала утверждаем тип с помощью myVar.(int). Into v присваивается типизированная переменная, если утверждение типа было успешным, а в ok назначается логическое значение того, было ли утверждение успешным.

Есть еще несколько утверждений типа, если вам интересно, я рекомендую Go Tour для утверждений типов и переключателей типов.

Где детали типа?

Как утверждение типа (а также по доверенности, отражение) действительно знает базовый тип более общего интерфейса (пустой интерфейс или иначе).

Чтобы понять это, мы посмотрим непосредственно на реализацию go на момент написания в go / src / sync / atomic / value.go. Это реализует базовое значение каждой переменной на ходу.

// ifaceWords is interface{} internal representation.                       type ifaceWords struct {
    typ  unsafe.Pointer
    data unsafe.Pointer
}

Каждый пустой интерфейс и, соответственно, каждое значение в go имеет в своем базовом представлении 2 небезопасных указателя: typ и data.

typ содержит информацию о типе для текущей переменной, поэтому, даже если переменная может быть пустым интерфейсом, фактическая информация о типе внутри typ остается неизменной.

data содержит само значение, а также другие данные, такие как kind, которые выходят за рамки данной статьи. Важным моментом является то, что data содержит значение * и * еще несколько вещей, тогда как typ содержит только данные типа.

Чего не хватает в утверждениях типа? (Или: зачем нам отражения?)

Утверждения типа позволяют проверять и использовать базовый тип для интерфейсов, когда вы знаете, какой тип проверять.

В приведенном выше примере мы используем assert специально для int. Это означает, что мы заранее знали тип, чтобы утверждение типа работало. Даже если мы switch и проверим несколько параметров, мы все равно должны знать конкретный тип, который мы утверждаем во время компиляции.

В тех случаях, когда мы не знаем конкретных типов во время компиляции, возникают размышления. Или, другими словами, как указано в начале этой статьи, когда нам нужно провести проверки во время выполнения.

Вернемся к примеру fmt.Print: пакет fmt не знает о типах наших структур, но все же может распечатать его значение и тип (используя %T).

Наконец: размышления

Reflect.Type и Reflect.Value

Это два основных и наиболее важных типа, предоставляемых пакетом Reflect. Reflect.Type и Reflect.Value - это структуры, определенные только в Reflect пакете, а в пакете отражения есть методы, работающие с интерфейсными переменными, которые внутренне заполняют эти структуры, используя базовые typ и data, доступные каждому интерфейсу на ходу.

Reflect.TypeOf() и Reflect.ValueOf() - два доступных основных метода, возвращающих соответственно Reflect.Type и Reflect.Value, как показано в этом примере:

Для большей ясности стоит взглянуть на тот же пример с нашими собственными созданными структурами:

Использование TypeOf и ValueOf возвращает нам базовый тип и указатель на значение. Важно понимать, что значение, поступающее из ValueOf, относится к типу Reflect.Value, а не к исходному типу переменной.

Так, например, в приведенном выше примере с int мы не можем брать значение и выполнять с ним арифметические операции, например myValue + 1. Он не будет скомпилирован, поскольку компилятор Go распознает это действие не для Reflect.Value, а только для целочисленных типов.

Мы узнаем, как вы можете снова использовать это значение, если потребуется.

Отражение. Вид

Понимание типов может быть непростым делом, а ресурсы, которые вы можете найти в Интернете, могут сбивать с толку, поскольку большинство из них рассматривают систему типов из теории типов и говорят о реализации на таких языках, как Haskell.

Что касается Go, вам нужно знать, что каждая переменная имеет вид, производный от ее типа. Виды подобны типам типов.

Проще всего продемонстрировать места, где вы создаете свои собственные типы: структуры.

Вернемся к приведенному выше коду, в котором мы создаем структуру Person. Человек, которого мы создаем, имеет тип Person. Тип типа Person, а именно его Kind, равен Struct.

Чтобы получить тип типа, примените метод Kind() к переменной Reflect.Type.

Вы можете увидеть полный список доступных видов здесь: https://golang.org/pkg/reflect/#Kind

В некоторых случаях, кроме очевидного примера структур, типы выглядят как дублирование - например, тип int64 имеет вид int64. Не подчеркивайте это, для нашей цели знание этого типа полезно для повторного использования пустого значения интерфейса (поскольку Reflect.Value не может использоваться сам по себе, как показано в разделе выше).

Преобразование Reflect.Value обратно в значение исходного типа

Итак, у нас есть значение типа Reflect.Value после того, как мы проанализировали пустую переменную интерфейса, переданную нам с помощью Reflect.ValueOf(). Но это значение на самом деле непригодно, так как система типов Go еще не распознает его как исходный тип.

Мы хотим преобразовать само значение в исходный тип. Процесс для этого будет:

  1. Определите точный тип оригинала. При необходимости используйте Reflect.TypeOf() или Reflect.Kind().
  2. Получите необработанные данные значения, используя указатель на значение. (Это небезопасный указатель, но на данный момент это выходит за рамки)
  3. Введите приведение указателя.

К счастью для нас, пакет reflect уже справляется с этим для всех основных типов.

Основные методы преобразования типов

Пакет reflect предоставляет метод reflect.Value, который преобразует значение обратно в типизированное исходное значение. Давайте посмотрим, как это работает с целым числом:

Доступны методы преобразования для всех основных типов: Bool, Float, String и т. Д. И т. Д.

Исследование сложных типов

Вышеупомянутое охватывает основные типы, но что мы делаем, например, со структурами?

Пакет reflect также предоставляет методы для исследования структур «извне». Получение количества полей и получение отдельных полей.

Пример кода прояснит:

Спасибо!

Спасибо, что уделили время чтению этой статьи, я искренне надеюсь, что она была полезной (:

Следите за мной в твиттере, я пишу о самых разных технологиях: https://twitter.com/snird