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

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

Следует отметить, что в большинстве задач, стоящих перед разработчиком, рефлексия не применяется. Тем не менее она используется в популярном пакете encoding/json и многих других. Это хороший аргумент, чтобы изучить рефлексию и понять, как она работает.

Note

Для работы с рефлексией в языке Go есть пакет reflect из стандартной библиотеки.

В этом уроке вы познакомитесь с некоторыми возможностями пакета reflect. Функции пакета работают с произвольными статическими типами (interface{}) и позволяют получать метаинформацию о них. С помощью этого пакета можно динамически создавать типы в ходе выполнения приложения (в runtime).

DeepEqual

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

Рассмотрим простой пример наивного подхода:

type MyType struct {
    IntField   int
    StrField   string
    PtrField   *float64
}
 
func (mt MyType) IsEqual(mt2 MyType) bool {
    return mt == mt2
}
 
func main() {
    floatValue1, floatValue2 := 10.0, 10.0
    a := MyType{IntField: 1, StrField: "str", PtrField: &floatValue1}
    b := MyType{IntField: 1, StrField: "str", PtrField: &floatValue2}
 
    fmt.Printf("Равенство a и b: %v\n", a.IsEqual(b))
}
Равенство a и b: false

Получаем false, хотя внешне содержимое этих переменных кажется равным. В чём подвох?

В примере определены тип MyType и метод для сравнения двух экземпляров типа IsEqual.

Выполнение этого сниппета выводит в консоль false, хотя значения всех полей объектов a и b равны. Это происходит потому, что прямое сравнение указателей (поле PtrField) сопоставляет адреса, но не значения. Если оба указателя будут иметь значения nil, то код выведет true.

Изменим спецификацию типа:

type MyType struct {
    IntField   int
    StrField   string
    PtrField   *float64
    SliceField []int
} 

Компиляция такого кода выведет ошибку, так как для типа SliceField не определена операция ==. Добавление в структуру поля ссылочного типа (или в поле вложенной структуры) приводит к тому, что использовать оператор == для её прямого сравнения невозможно. Это справедливо не только для структур, но и для всех пользовательских типов, например type MySlice []int.

Note

Одно из решений — изменить код метода IsEqual и добавить туда несколько if. Написание метода или функции сравнения — распространённая практика в Go, так как язык не позволяет перегружать операторы ( == в данном случае).

Пакет reflect предлагает следующее решение:

func (mt MyType) IsEqual(mt2 MyType) bool {
    return reflect.DeepEqual(mt, mt2)
}
 
func main() {
    floatValue1, floatValue2 := 10.0, 10.0
    a := MyType{IntField: 1, StrField: "str", PtrField: &floatValue1, SliceField: []int{1}}
    b := MyType{IntField: 1, StrField: "str", PtrField: &floatValue2, SliceField: []int{1}}
 
    fmt.Printf("Равенство a и b: %v\n", a.IsEqual(b))
} 
Равенство 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: структуру, канал, слайс, функцию, массив и другие.

var varBool *bool
fmt.Println(reflect.ValueOf(varBool).Kind()) // ptr — указатель
fmt.Println(reflect.ValueOf(varBool).Type()) // *bool — указатель на bool
 
var varFloat float32
fmt.Println(reflect.ValueOf(varFloat).Kind()) // float32
fmt.Println(reflect.ValueOf(varFloat).Type()) // float32
 
var varMap map[string]int
fmt.Println(reflect.ValueOf(varMap).Kind()) // map  
fmt.Println(reflect.ValueOf(varMap).Type()) // map[string]int
 
varStruct := struct{Value int}{}
fmt.Println(reflect.ValueOf(varStruct).Kind()) // struct
fmt.Println(reflect.ValueOf(varStruct).Type()) // struct { Value int } 

Тип reflect.Type содержит описание Go-типа. Тип reflect.Kind задаёт множество базовых типов Go: структуру, канал, слайс, функцию, массив и другие. То есть Type описывает, каким конкретно типом является значение, а Kind — каким видом типа он является.

Довольно полезная вещь, чтобы понять, что нам передали: структуру, массив или просто целое число.

Проверка на nil

В прошлом уроке мы затрагивали сравнение интерфейсов с nil и выяснили, что nil, может быть как значением самого интерфейса, так и значением величины, на которую он указывает.

Попробуем реализовать наивную реализацию сравнения.

type MyType struct{}
 
func NaiveIsNil(obj interface{}) bool {
    return obj == nil
}
 
func main() {
    var t *MyType
    fmt.Printf("Проверка типа (%v) на nil: %v\n", reflect.TypeOf(t), NaiveIsNil(t)) // TypeOf возвращает тип переданного объекта. 
} 
Проверка типа (*main.MyType) на nil: false 

Что-то явно пошло не так. Мы передали в функцию nil и ожидали true.

Переменная t — указатель на тип MyType. При таком объявлении переменной не задаётся значение указателя, и технически он пуст, но прямое сравнение с nil выдаёт false. Причина была рассмотрена в предыдущем уроке, поэтому сейчас не будем останавливаться на этом подробно. Используя пакет reflect, напишем универсальное решение для подобной проверки:

func IsNil(obj interface{}) bool {
    if obj == nil {
        return true
    }
 
    objValue := reflect.ValueOf(obj)
    // проверяем, что тип значения ссылочный, то есть в принципе может быть равен nil
    if objValue.Kind() != reflect.Ptr {
        return false
    } 
    // проверяем, что значение равно nil 
    //  важно, что IsNil() вызывает панику, если value не является ссылочным типом. Поэтому всегда проверяйте на Kind() 
    if  objValue.IsNil() {
        return true
    }
 
    return false
}

Вот ещё несколько полезных методов:

varInt := 100
varIntValue := reflect.ValueOf(varInt)
fmt.Println(varIntValue.IsZero()) // false
fmt.Println(varIntValue.Int())    // 100
 
var varPtr *int
varPtrValue := reflect.ValueOf(varPtr)
fmt.Println(varPtrValue.IsNil())  // true
fmt.Println(varPtrValue.IsZero()) // true 

Метод IsZero() сравнивает значение со значением по умолчанию (для int — 0, для указателя — nil и т. д.).

Метод IsNil() сравнивает значение с nil и применим только к типам, которые поддерживают nil (chan, slice, map и т. д.).

Как и с другими типами пакета, с Value нужно обращаться осторожно, потому что неверное использование его методов приведёт к панике. Например:

var varBool *bool
varBoolValue := reflect.ValueOf(varBool)
fmt.Println(varBoolValue.IsNil())        // true
fmt.Println(varBoolValue.IsZero())       // true
fmt.Println(varBoolValue.Elem().Bool())  // panic: попытка получить значение для пустого Value 

Метод Elem() возвращает значение (тоже тип Value), которое описывается интерфейсом Value (varBoolValue в данном случае).

По сути, метод Elem() — это разыменование: возвращается значение, на которое указывает Value, бывший указателем.

Исправим предыдущий пример и покажем, как выставить значение для указателя через рефлексию:

var varBool *bool
fmt.Println(reflect.ValueOf(varBool).IsNil())  // true
 
trueVal := true
reflect.ValueOf(&varBool).Elem().Set(reflect.ValueOf(&trueVal))
 
fmt.Println(reflect.ValueOf(varBool).IsNil())       // false
fmt.Println(reflect.ValueOf(varBool).Elem().Bool()) // true — получить значение через рефлексию
fmt.Println(*varBool)                               // true — получить значение без рефлексии 

Выставляем значение переменной varBool, используя метод Set(val reflect.Value) и передавая в него подходящее по типу Value.

Fields и NumFields — итерация по полям структуры

Если вы когда-либо писали на низкоуровневом С, то наверняка вам приходилось работать со структурами как с массивами байт. Просто брали указатель на структуру и двигались по ней этим указателем, получая доступ к полям. Процесс крайне опасный и неудобный, однако, надо признать, крайне эффективный в части производительности.

Note

В Python тоже есть возможность получить доступ к элементам объекта класса как к словарю. В Go тоже хотелось бы иметь такую возможность. Пакет reflect её также предоставляет, однако следует отметить, что производительность такого решения ниже, чем обычного доступа к полям структур.

Рассмотрим на примере:

package main
 
import "reflect"
 
//
func ExtendedPrint(v interface{}) {
    val := reflect.ValueOf(v)
    //  проверяем, а не передали ли нам указатель на структуру
    switch val.Kind() {
    case reflect.Ptr:
        if val.Elem().Kind() != reflect.Struct {
            fmt.Printf("Pointer to %v : %v", val.Elem().Type(), val.Elem())
            return
        }
        // если всё-таки это указатель на структуру, дальше будем работать с самой структурой
        val = val.Elem()
 
    case reflect.Struct: // работаем со структурой
    default:
        fmt.Printf("%v : %v", val.Type(), val)
        return
    }
 
    fmt.Printf("Struct of type %v and number of fields %d:\n", val.Type(), val.NumField())
    for fieldIndex := 0; fieldIndex < val.NumField(); fieldIndex++ {
        field := val.Field(fieldIndex) // field — тоже Value
        fmt.Printf("\tField %v: %v - val :%v\n", val.Type().Field(fieldIndex).Name, field.Type(), field)
        // имя поля мы получаем не из значения поля, а из его типа. 
    }
}
 
func main() {
    s := MyStruct{
        A: 3,
        B: "some",
        C: false,
    }
    s1 := &MyStruct{
        A: 7,
        B: "text",
        C: true,
    }
 
    ExtendedPrint(s)
    ExtendedPrint(s1)
    ExtendedPrint(struct {
        E int
        C string
    }{2, "other text"})
    ExtendedPrint("some string")
}

Вывод будет таким:

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().

Например, реализуем функцию, которая определяет, есть ли у входной структуры поле с именем. И если такое поле есть, она его изменит.

func ChangeFieldByName(v interface{}, fname string, newval int) {
    val := reflect.ValueOf(v)
    if val.Kind() == reflect.Ptr {
        val = val.Elem()
    }
    if val.Kind() != reflect.Struct {
        return
    }
 
    field := val.FieldByName(fname)
    if field.IsValid() {
        if field.CanSet() {
            switch field.Kind() {
            case reflect.Int:
                field.SetInt(int64(newval))
            case reflect.String:
                field.SetString(strconv.Itoa(newval))
            }
        }
    }
}
 

Tip

CanSet проверяет, отразится ли изменение Value на переданной ей переменной. Если переменная передана в ValueOf по значению, то функция не сработает. Ещё один важный момент: изменить значение можно только для экспортируемых полей.

Динамическая информация о типе (парсинг тегов)

Для каждого поля структуры можно задать теги — строку с дополнительной информацией по этому полю. Например, можно указать настройки сериализации в формат JSON. Теги задаются в формате key:value, причём может быть задано несколько тегов, разделённых пробелами. При парсинге тегов они преобразуются в набор пар «ключ-значение».

Обычно теги используются тогда, когда недостаточно информации о поле структуры, полученной через рефлексию. К примеру, хотелось бы иметь точное название поля структуры для сериализации.

Используя пакет reflect, напишем функцию GetStructTags, которая вернёт информацию обо всех заданных тегах структуры и их значениях:

type (
    // FieldsInfo содержит информацию о полях структуры (ключ: имя поля).
    FieldsInfo map[string]FieldInfo
 
    // FieldInfo содержит информацию о поле структуры.
    FieldInfo struct {
        // тип поля
        Type     string     `json:"type"`
        // теги
        Tags     TagsInfo   `json:"tags,omitempty"`
        // информация по полям вложенной структуры
        Embedded FieldsInfo `json:"embedded,omitempty"`
    }
 
    // TagsInfo содержит информацию о тегах (ключ: имя тега).
    TagsInfo map[string][]string
)
 
// String возвращает строковую репрезентацию типа FieldsInfo.
func (f FieldsInfo) String() string {
    bz, _ := json.MarshalIndent(f, "", "   ")
    return string(bz)
}
 
// GetStructTags возвращает информацию по каждому полю структуры.
func GetStructTags(obj interface{}) (retInfos FieldsInfo) {
    retInfos = make(FieldsInfo)
 
    // получаем описание типа переданного объекта
    // далее по коду явно передаём в функцию тип `reflect.Type`, поддержим здесь этот случай рекурсивного вызова
    var objType reflect.Type
    if t, ok := obj.(reflect.Type); ok {
        objType = t
    } else {
        objType = reflect.ValueOf(obj).Type()
    }
 
    // чиним вход: если передали указатель, получим описание типа под указателем
    if objType.Kind() == reflect.Ptr {
        objType = objType.Elem()
    }
 
    // проверка входа: если объект не структура, искать теги не нужно
    if objType.Kind() != reflect.Struct {
        return
    }
 
    // итерируемся по всем полям структуры
    // NumField() — возвращает количество полей в структуре
    for fieldIdx := 0; fieldIdx < objType.NumField(); fieldIdx++ {
        field := objType.Field(fieldIdx) // получаем поле структуры
        retInfos[field.Name] = FieldInfo{
            Type:     field.Type.String(), // тип структуры
            Tags:     parseTagString(string(field.Tag)), // теги структуры
            Embedded: GetStructTags(field.Type), // рекурсивно вызываем для каждого поля эту же функцию; если поле — структура, то пройдёмся и по ней.
        }
    }
 
    return
}
 
// parseTagString десериализует тег-строку поля структуры.
// Дедупликация имён тегов: первый по порядку (слева направо).
// Ограничения: значение тега не может содержать символы ':' и '"'.
func parseTagString(tagRaw string) (retInfos TagsInfo) {
    retInfos = make(TagsInfo)
 
    // пример строки: json:"name" pg:"nullable,sortable"
    for _, tag := range strings.Split(tagRaw, " ") {
        if tag = strings.TrimSpace(tag); tag == "" {
            continue
        }
 
        tagParts := strings.Split(tag, ":")
        if len(tagParts) != 2 {
            continue
        }
 
        tagName := strings.TrimSpace(tagParts[0])
        if _, found := retInfos[tagName]; found {
            continue
        }
 
        tagValuesRaw, _ := strconv.Unquote(tagParts[1])
        tagValues := make([]string, 0)
        for _, value := range strings.Split(tagValuesRaw, ",") {
            if value := strings.TrimSpace(value); value != "" {
                tagValues = append(tagValues, value)
            }
        }
 
        retInfos[tagName] = tagValues
    }
 
    return
}

Пример выполнения:

type (
    TestStruct struct {
        Id        string `json:"id" format:"uuid" example:"68b69bd2-8db6-4b7f-b7f0-7c78739046c6"`
        Name      string `json:"name" example:"Bob"`
        Group     Group  `json:"group"`
        CreatedAt int64  `json:"created_at" format:"unix" example:"1622647813"`
    }
 
    Group struct {
        Id             uint64   `json:"id"`
        PermsOverrides []string `json:"overrides" example:"USERS_RW,COMPANY_RWC"`
    }
)
 
func main() {
    var s *TestStruct
    fmt.Println(GetStructTags(s))
} 
{
   "CreatedAt": {
      "type": "int64",
      "tags": {
         "example": [
            "1622647813"
         ],
         "format": [
            "unix"
         ],
         "json": [
            "created_at"
         ]
      }
   },
   "Group": {
      "type": "main.Group",
      "tags": {
         "json": [
            "group"
         ]
      },
      "embedded": {
         "Id": {
            "type": "uint64",
            "tags": {
               "json": [
                  "id"
               ]
            }
         },

Ключевые мысли урока

Мы рассмотрели основные возможности применения рефлексии в языке Go. Рефлексия позволяет на основе переданной переменной узнать тип и значение этой переменной. Благодаря этому можно создавать гибкий код, работающий с очень широким набором типов входных данных. Однако следует помнить, что рефлексия замедляет работу программы. По возможности следует использовать другие подходы, так как работать такая программа будет крайне медленно.

Кроме того, мы исследовали:

  • Определение типа переменной.
  • Определение имён и количества полей структуры, а также доступ к ним по индексу.
  • Изменение переменной через рефлексию.
  • Использование тегов.

📂 Go | Последнее изменение: 30.08.2024 19:55