Рефлексией в программировании называют возможность получить информацию о типе из переменной этого типа. Проще говоря, рефлексия позволяет получать информацию о коде программы и менять его во время выполнения.
Рефлексия обычно применяется для работы с данными, тип которых неизвестен при компиляции. Например, по сети могут приходить какие-то данные, которые должны быть уложены в структуры. Однако можно не знать, что это за данные, поэтому в таких случаях напрашивается возможность создавать структуры данных на лету.
Следует отметить, что в большинстве задач, стоящих перед разработчиком, рефлексия не применяется. Тем не менее она используется в популярном пакете encoding/json
и многих других. Это хороший аргумент, чтобы изучить рефлексию и понять, как она работает.
Note
Для работы с рефлексией в языке Go есть пакет reflect из стандартной библиотеки.
В этом уроке вы познакомитесь с некоторыми возможностями пакета reflect
. Функции пакета работают с произвольными статическими типами (interface{}
) и позволяют получать метаинформацию о них. С помощью этого пакета можно динамически создавать типы в ходе выполнения приложения (в runtime).
DeepEqual
Иногда возникает потребность сравнить две переменные одного типа по значению, и бывают случаи, что простой подход с использованием ==
не срабатывает. Тогда нужно заглянуть глубже, сравнить все значения, лежащие в слайсах и мапах, под указателями.
Рассмотрим простой пример наивного подхода:
Равенство a и b: false
Получаем false
, хотя внешне содержимое этих переменных кажется равным. В чём подвох?
В примере определены тип MyType
и метод для сравнения двух экземпляров типа IsEqual
.
Выполнение этого сниппета выводит в консоль false
, хотя значения всех полей объектов a
и b
равны. Это происходит потому, что прямое сравнение указателей (поле PtrField
) сопоставляет адреса, но не значения. Если оба указателя будут иметь значения nil
, то код выведет true
.
Изменим спецификацию типа:
Компиляция такого кода выведет ошибку, так как для типа SliceField
не определена операция ==
. Добавление в структуру поля ссылочного типа (или в поле вложенной структуры) приводит к тому, что использовать оператор ==
для её прямого сравнения невозможно. Это справедливо не только для структур, но и для всех пользовательских типов, например type MySlice []int
.
Note
Одно из решений — изменить код метода
IsEqual
и добавить туда несколькоif
. Написание метода или функции сравнения — распространённая практика в Go, так как язык не позволяет перегружать операторы ( == в данном случае).
Пакет reflect
предлагает следующее решение:
Равенство a и b: true
Функция сравнивает значения всех элементов типа, включая вложенные. В документации можно ознакомиться со всеми критериями для сравнения функции DeepEqual
.
На практике DeepEqual
используется нечасто, так как вызов этой функции рекурсивно пробегает по всем элементам типа, на что уходит много времени процессора. Чаще всего IsEqual
пишут вручную, ограничивая область сравнения требуемой логикой приложения.
DeepEqual
позволяет сравнить две переменные одного типа по значению, даже если эти переменные имеют сложную структуру данных, например содержат ссылки на другие переменные.
Value и ValueOf()
В прошлом уроке мы обсуждали приведение интерфейсов. Интерфейс одного типа можно попробовать привести к другому типу. Однако что, если мы не знаем, к какому типу нужно привести переданный объект?
Пакет reflect
представляет расширенные возможности по работе с приведением типов и их исследованию во время исполнения программы.
Представьте, что мы пишем некую библиотеку, которая должна работать с разнообразными типами данных. В качестве примера можно привести пакет encoding/json
, который умеет принимать и сериализировать любые структуры.
Одного приведения здесь явно недостаточно, ведь перед разработчиком зачастую встают, например, такие вопросы:
- Сколько полей у структуры?
- Какой у них тип?
- Как они называются? Хочется получить эти названия в виде строки.
В то же время структура передаётся в нашу библиотеку, завёрнутой в пустой интерфейс.
Именно на эти вопросы призвана ответить рефлексия.
Каждое значение, вне зависимости от типа, можно привести к универсальному типу reflect.Value
. Делается это через вызов reflect.ValueOf(v inteface()) Value
. Эта функция принимает некоторое значение и возвращает Value
. У самого же Value
много методов, которые позволяют получить информацию как о типе, так и о значении.
Рассмотрим некоторые из них.
Type и Kind
Метод Type()
возвращает тип объекта.
Метод Kind()
возвращает базовый тип объекта, то есть не пользовательский тип, а один из встроенных в язык Go: структуру, канал, слайс, функцию, массив и другие.
Тип reflect.Type
содержит описание Go-типа. Тип reflect.Kind
задаёт множество базовых типов Go: структуру, канал, слайс, функцию, массив и другие. То есть Type
описывает, каким конкретно типом является значение, а Kind
— каким видом типа он является.
Довольно полезная вещь, чтобы понять, что нам передали: структуру, массив или просто целое число.
Проверка на nil
В прошлом уроке мы затрагивали сравнение интерфейсов с nil
и выяснили, что nil
, может быть как значением самого интерфейса, так и значением величины, на которую он указывает.
Попробуем реализовать наивную реализацию сравнения.
Проверка типа (*main.MyType) на nil: false
Что-то явно пошло не так. Мы передали в функцию nil
и ожидали true
.
Переменная t
— указатель на тип MyType
. При таком объявлении переменной не задаётся значение указателя, и технически он пуст, но прямое сравнение с nil
выдаёт false
. Причина была рассмотрена в предыдущем уроке, поэтому сейчас не будем останавливаться на этом подробно. Используя пакет reflect
, напишем универсальное решение для подобной проверки:
Вот ещё несколько полезных методов:
Метод IsZero()
сравнивает значение со значением по умолчанию (для int
— 0, для указателя — nil
и т. д.).
Метод IsNil()
сравнивает значение с nil
и применим только к типам, которые поддерживают nil
(chan
, slice
, map
и т. д.).
Как и с другими типами пакета, с Value
нужно обращаться осторожно, потому что неверное использование его методов приведёт к панике. Например:
Метод Elem()
возвращает значение (тоже тип Value
), которое описывается интерфейсом Value
(varBoolValue
в данном случае).
По сути, метод Elem()
— это разыменование: возвращается значение, на которое указывает Value
, бывший указателем.
Исправим предыдущий пример и покажем, как выставить значение для указателя через рефлексию:
Выставляем значение переменной varBool
, используя метод Set(val reflect.Value)
и передавая в него подходящее по типу Value
.
Fields и NumFields — итерация по полям структуры
Если вы когда-либо писали на низкоуровневом С, то наверняка вам приходилось работать со структурами как с массивами байт. Просто брали указатель на структуру и двигались по ней этим указателем, получая доступ к полям. Процесс крайне опасный и неудобный, однако, надо признать, крайне эффективный в части производительности.
Note
В Python тоже есть возможность получить доступ к элементам объекта класса как к словарю. В Go тоже хотелось бы иметь такую возможность. Пакет
reflect
её также предоставляет, однако следует отметить, что производительность такого решения ниже, чем обычного доступа к полям структур.
Рассмотрим на примере:
Вывод будет таким:
Struct of type main.MyStruct and number of fields 3:
Field A: int - val :3
Field B: string - val :some
Field C: bool - val :false
Struct of type main.MyStruct and number of fields 3:
Field A: int - val :7
Field B: string - val :text
Field C: bool - val :true
Struct of type struct { E int; C string } and number of fields 2:
Field E: int - val :2
Field C: string - val :other text
string : some string
Получаем довольно удобную и интересную особенность, которая часто используется для исследования переданных структур. Если вам нужно получить доступ к одному из полей переданной структуры по имени, используйте полезные функции:
FieldByName(name string)
— возвращаетValue
поля структуры по имени.FieldByIndex(i int)
— возвращает поле структуры по индексу.
Для методов типа есть аналогичные функции.
Изменение поля структуры
С помощью рефлексии можно изменить переданный объект.
Однако не всякий объект может быть изменён. Чтобы узнать, можно ли изменить Value
, используется метод CanSet()
.
Например, реализуем функцию, которая определяет, есть ли у входной структуры поле с именем. И если такое поле есть, она его изменит.
Tip
CanSet
проверяет, отразится ли изменениеValue
на переданной ей переменной. Если переменная передана вValueOf
по значению, то функция не сработает. Ещё один важный момент: изменить значение можно только для экспортируемых полей.
Динамическая информация о типе (парсинг тегов)
Для каждого поля структуры можно задать теги — строку с дополнительной информацией по этому полю. Например, можно указать настройки сериализации в формат JSON. Теги задаются в формате key:value
, причём может быть задано несколько тегов, разделённых пробелами. При парсинге тегов они преобразуются в набор пар «ключ-значение».
Обычно теги используются тогда, когда недостаточно информации о поле структуры, полученной через рефлексию. К примеру, хотелось бы иметь точное название поля структуры для сериализации.
Используя пакет reflect
, напишем функцию GetStructTags
, которая вернёт информацию обо всех заданных тегах структуры и их значениях:
Пример выполнения:
Ключевые мысли урока
Мы рассмотрели основные возможности применения рефлексии в языке Go. Рефлексия позволяет на основе переданной переменной узнать тип и значение этой переменной. Благодаря этому можно создавать гибкий код, работающий с очень широким набором типов входных данных. Однако следует помнить, что рефлексия замедляет работу программы. По возможности следует использовать другие подходы, так как работать такая программа будет крайне медленно.
Кроме того, мы исследовали:
- Определение типа переменной.
- Определение имён и количества полей структуры, а также доступ к ним по индексу.
- Изменение переменной через рефлексию.
- Использование тегов.
📂 Go | Последнее изменение: 30.08.2024 19:55