Привет, Хабр! Представляю вашему вниманию перевод статьи «The Laws of Reflection» от создателя языка.

Рефлексия — способность программы исследовать собственную структуру, в особенности через типы. Это форма метапрограммирования и отличный источник путаницы.
В Go рефлексия широко используется, например, в пакетах test и fmt. В этой статье попытаемся избавиться от «магии», объяснив, как рефлексия работает в Go.

Типы и Интерфейсы


Так как рефлексия основывается на системе типов, давайте освежим знания о типах в Go.
Go статически типизирован. Каждая переменная имеет один и только один статический тип, зафиксированный во время компиляции: int, float32, *MyType, []byte… Если мы объявляем:

type MyInt int
var i int
var j MyInt

то i имеет тип int и j имеет тип MyInt. Переменные i и j имеют разные статические типы и, хотя они имеют один и тот же базовый тип, они не могут быть присвоены друг другу без преобразования.

Одной из важных категорий типа являются интерфейсы, которые представляют собой фиксированные множества методов. Интерфейс может хранить любое конкретное (неинтерфейсное) значение, пока это значение реализует методы интерфейса. Известной парой примеров является io.Reader и io.Writer, типы Reader и Writer из пакета io:

// Reader - это интерфейс, оборачивающий базовый метод Read().
type Reader interface {
    Read(p []byte) (n int, err error)
}
// Writer - это интерфейс, оборачивающий базовый метод Write().
type Writer interface {
    Write(p []byte) (n int, err error)
}

Говорят, что любой тип, который реализует метод Read() или Write() с этой сигнатурой, реализует io.Reader или io.Writer соответственно. Это означает, что переменная типа io.Reader может содержать любое значение, тип которого имеет метод Read():

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)

Важно понять, что r может присваиваться любое реализующее io.Reader значение. Go статически типизирован, а статический тип rio.Reader.

Чрезвычайно важным примером типа интерфейса является пустой интерфейс:

interface{}

Он представляет собой пустое множество ? методов и реализуется любым значением.
Некоторые говорят, что интерфейсы Go являются переменными с динамической типизацией, но это заблуждение. Они статически типизированы: переменная с типом интерфейс всегда имеет один и тот же статический тип, и хотя во время выполнения значение, хранящееся в переменной интерфейса, может изменять тип, это значение всегда будет удовлетворять интерфейсу. (Никаких undefined, NaN и прочих ломающих логику программы вещей.)

Это надо понять — отражение и интерфейсы тесно связаны.

Внутреннее представление интерфейса


Russ Cox написал подробное сообщение в блоге о утройстве интерфейса в Go. Не менее хорошая статья есть на Habr'е. Здесь нет необходимости повторять всю историю, основные моменты упомянуты.

Переменная типа интерфейса хранит пару: конкретное значение, присвоенное переменной, и дескриптор типа этого значения. Точнее, значение — базовый элемент данных, который реализует интерфейс, а тип описывает полный тип этого элемента. Например, после

var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    return nil, err
}
r = tty

r содержит, схематически, пару (значение, тип) --> (tty, *os.File). Обратите внимание, что тип *os.File реализует методы, отличные от Read(); даже если значение интерфейса обеспечивает доступ только к методу Read(), значение внутри несет всю информацию о типе этого значения. Вот почему мы можем делать такие вещи:

var w io.Writer
w = r.(io.Writer)

Выражение в этом присваивании является утверждением типа; оно утверждает, что элемент внутри r также реализует io.Writer, и поэтому мы можем назначить его w. После назначения w будет содержать пару (tty, *os.File). Это та же пара, что и в r. Статический тип интерфейса определяет, какие методы могут быть вызваны у интерфейсной переменной, хотя конкретное значение внутри может иметь более широкий набор методов.

Продолжая, мы можем сделать следующее:

var empty interface{}
empty = w

и пустое значение пустого поля снова будет содержать ту же пару (tty, *os.File). Это удобно: пустой интерфейс может содержать любое значение и всю информацию, которая нам когда-либо понадобится от него.

Нам здесь не нужно утверждение типа, потому что известно, что w удовлетворяет пустому интерфейсу. В примере, где мы перенесли значение из Reader в Writer, нам нужно было явно использовать утверждение типа, потому что методы Writer'а не являтся подмножеством Reader'а. Попытка преобразовать значение, которое не соответствует интерфейсу, вызовет панику.

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

Теперь мы готовы изучить reflect.

Первый закон отражения reflect


  • Reflection распространяется от интерфейса до reflection объекта.

На базовом уровне reflect является всего лишь механизмом для изучения пары тип и значение, хранящейся внутри переменной интерфейса. Чтобы начать работу, есть два типа, о которых нам нужно знать: reflect.Type и reflect.Value. Эти два типа предоставляют доступ к содержимому интерфейсной переменной и возвращаются простыми функциями, reflect.TypeOf() и reflect.ValueOf() соответственно. Они выделяют части из значения интерфейса. (Кроме того, из reflect.Value легко получить reflect.Type, но давайте не будем смешивать концепции Value и Type на данный момент.)

Начнем с TypeOf():

package main
import (
    "fmt"
    "reflect"
)

func main() {
    var x float64 = 3.4
    fmt.Println("type:", reflect.TypeOf(x))
}

Программа выведет
type: float64

Программа похожа на передачу простой переменной float64 x в reflect.TypeOf(). Вы видете интерфейс? А он есть — reflect.TypeOf() принимает пустой интерфейс, согласно объявлению функции:

// TypeOf() возвращает reflect.Type переменной в пустой интерфейс.
func TypeOf(i interface{}) Type

Когда мы вызываем reflect.TypeOf(x), x сначала сохраняется в пустом интерфейсе, который затем передается в качестве аргумента; reflect.TypeOf() распаковывает этот пустой интерфейс для восстановления информации о типе.

Функция reflect.ValueOf(), конечно же, восстанавливает значение (далее мы будем игнорировать шаблон и сосредоточимся на коде):

var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())

напечатает
value: <float64 Value>
(Мы вызываем метод String() явно, потому что по умолчанию пакет fmt распаковывает в reflect.Value и выводит конкретное значение.)
И reflect.Type, и reflect.Value имеют много методов, что позволяет исследовать и изменять их. Одним из важных примеров является то, что reflect.Value имеет метод Type(), который возвращает тип значения. reflect.Type и reflect.Value имеют метод Kind(), который возвращает константу, указывающую, какой примитивный элемент хранится: Uint, Float64, Slice… Эти константы объявлены в перечислении в пакете reflect. Методы Value с такими именами, как Int() и Float(), позволяют нам вытащить значения (как int64 и float64), заключённые внутри:

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

напечатает

type: float64
kind is float64: true
value: 3.4

Существуют также методы, такие как SetInt() и SetFloat(), но для их использования нам необходимо понять устанавливаемость (settability), тему третьего закона отражения.

Библиотека reflect имеет пару свойств, которые нужно выделить. Во-первых, чтобы API был прост, «getter» и «setter» методы Value действуют на самый большой тип, который может содержать значение: int64 для всех целых чисел со знаком. То есть метод Int() значения Value возвращает int64, а значение SetInt() принимает int64; может потребоваться преобразование в фактический тип:

var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8)
x = uint8(v.Uint())  // v.Uint вернёт uint64.

будет

type: uint8
kind is uint8: true

Здесь v.Uint() вернёт uint64, необходимо явное утверждение типа.

Второе свойство состоит в том, что Kind() reflect объекта описывает базовый тип, а не статический тип. Если объект отражения содержит значение определяемого пользователем целочисленного типа, как в

type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x) // v имеет тип Value.

v.Kind() == reflect.Int, хотя статический тип x является MyInt, а не int. Другими словами, Kind() не может различать int из MyInt, в отличае от Type(). Kind может принимать только значения встроенных типов.

Второй закон отражения reflect


  • Reflection распространяется от reflect объекта до интерфейса.

Как и физическое отражение, reflect в Go создаёт свою противоположность.

Имея reflect.Value, мы можем восстановить значение интерфейса с помощью метода Interface(); метод упаковывает информацию о типе и значении обратно в интерфейс и возвращает результат:

// Interface вернёт значение v как interface{}.
func (v Value) Interface() interface{}
bvt
Как пример:

y := v.Interface().(float64) // y имеет тип float64.
fmt.Println(y)

напечатает значение float64, представленного reflect объектом v.
Однако мы можем сделать еще лучше. Аргументы в fmt.Println() и fmt.Printf() передаются как пустые интерфейсы, которые затем распаковываются пакетом fmt внутри, как и в предыдущих примерах. Поэтому все, что требуется для печати содержимого reflect.Value правильно — передать результат метода Interface() в функцию отформатированного вывода:

fmt.Println(v.Interface())

(Почему бы не fmt.Println(v)? Потому что v имеет тип reflect.Value; мы же хотим получить содержащееся внутри значение.) Поскольку наше значение — float64, мы можем даже использовать формат с плавающей запятой, если хотим:

fmt.Printf("value is %7.1e\n", v.Interface())

выведет в конкретном случае
3.4e+00

Опять же, нет необходимости приводить тип результата v.Interface() в float64; пустое значение интерфейса содержит информацию о конкретном значении внутри, а fmt.Printf() восстановит его.
Короче говоря, метод Interface() является инверсией функции ValueOf(), за исключением того, что его результат всегда имеет статический тип interface{}.

Повторим: Reflection распространяется от значений интерфейса к объектам reflection и обратно.

Третий закон отражения reflection


  • Чтобы изменить объект отражения, значение должно быть устанавливаемым.

Третий закон является самым тонким и запутанным. Начинаем с первых принципов.
Вот такой код не работает, но заслуживает внимания.

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Ошибка

Если вы запустите этот код, он упадёт с panic с критическим сообщением:
panic: reflect.Value.SetFloat использует неадресуемое значение
Проблема не в том, что литерал 7.1 не адресуется; это то, что v не устанавливаемо. Устанавливаемость — свойство reflect.Value, и не каждое reflect.Value имеет его.
Метод reflect.Value.CanSet() сообщает о устанавливаемости Value; в нашем случае:

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())

напечатает:
settability of v: false

Ошибка вызова метода Set() на неустанавливаемом значении. Но что такое устанавливаемость?

Устанавливаемость немного напоминает адресуемость, но строже. Это свойство, при котором reflection объект может изменить хранимое значение, которое было использовано при создании reflection объекта. Устанавливаемость определяется тем, содержит ли reflection объект исходный элемент, или только его копию. Когда мы пишем:

var x float64 = 3.4
v := reflect.ValueOf(x)

мы передаем копию x в reflect.ValueOf(), поэтому интерфейс создается как аргумент для reflect.ValueOf() — это копия x, а не сам x. Таким образом, если бы утверждение:

v.SetFloat(7.1)

было бы выполнено, оно бы не обновило x, хотя v выглядит так, как будто оно было создано из x. Вместо этого он обновил бы копию x, хранящуюся внутри значения v, a сам x не был бы затронут. Это запрещено, что бы не порождать проблем, а устанавливаемость — свойство, используемое для предотвращения проблемы.

Это не должно казаться странным. Это обычная ситуация в необычной одежде. Подумайте о передаче x в функцию:
f(x)

Мы не ожидаем, что f() сможет изменить x, потому что мы передали копию значения x, а не сам x. Если мы хотим, чтобы f() непосредственно меняла x, мы должны передать нашей функции указатель на x:
f(&x)

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

Давайте сделаем это. Сначала мы инициализируем x как обычно, а затем создаем reflect.Value p, которое указывает на него.

var x float64 = 3.4
p := reflect.ValueOf(&x) // Берём адрес x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

выведет
type of p: *float64
settability of p: false


Reflection объект p не может быть установлен, но это не p, который мы хотим установить, это указатель *p. Чтобы получить то, на что указывает p, мы вызываем метод Value.Elem(), который берёт значение косвенным образом через указатель, и сохраняем результат в reflect.Value v:

v := p.Elem()
fmt.Println("settability of v:", v.CanSet())

Теперь v является устанавливаемым объектом, резутьтат:
settability of v: true
и поскольку он представляет x, мы, наконец, можем использовать v.SetFloat() для изменения значения x:

v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

вывод, как ожидалось
7.1
7.1

Reflect может быть трудно понять, но он делает именно то, что делает язык, хотя и с помощью reflect.Type и reflection.Value, которые могут скрывать, что происходит. Просто имейте в виду, что reflection.Value нужен адрес переменной, чтобы изменить её.

Структуры


В нашем предыдущем примере v не был указателем, он был просто получен из него. Общим способом возникновения этой ситуации является использование reflection для изменения полей структуры. До тех пор, пока у нас есть адрес структуры, мы можем изменять её поля.

Вот простой пример, который анализирует значение структуры t. Мы создаем reflection объект с адресом структуры, чтобы изменять его позже. Затем устанавливаем typeOfT в его тип и итерируемся по полям, используя простые вызовы методов (см. Подробное описание пакета). Обратите внимание, что мы извлекаем имена полей из типа структуры, но сами поля являются обычными reflect.Value.

type T struct {
    A int
    B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
    f := s.Field(i)
    fmt.Printf("%d: %s %s = %v\n", i, typeOfT.Field(i).Name, f.Type(), f.Interface())
}

Программа выведет
0: A int = 23
1: B string = skidoo

Здесь проявляется еще один пункт об устанавливаемости: имена полей T в верхнем регистре (экспортируемы), потому что только экспортируемые поля устанавливаемы.
Поскольку s содержит устанавливаемый reflection объект, мы можем изменить поле структуры.

s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)

Результат:
t is now {77 Sunset Strip}
Если мы изменим программу так, чтобы s был создан из t, а не &t, вызовы SetInt() и SetString() завершились бы паникой, поскольку поля t не были бы устанавливаемыми.

Заключение


Вспомним законы reflection:

  • Reflection распространяется от интерфейса до reflection объекта.
  • Reflection распространяется от reflection объекта до интерфейса.
  • Чтобы изменить reflection объект, значение должно быть устанавливаемым.

Автор Rob Pike.

Комментарии (9)


  1. Hedgehogues
    26.06.2018 09:53

    Хорошо, получить типы — прикольно. Но остальное? Для чего всё это нужно рядовому разработчику, который не хочет экзерсисов? Я у себя в голове не смог это, к сожалению, уложить всё. Ну, да, есть пара прикольных методов. Но что с ними делать?


    1. rustler2000
      26.06.2018 13:04
      +1

      Парсеры, сериалайзеры, валидаторы и кастомные метаданные к структурам


    1. OlegSchwann Автор
      26.06.2018 15:38

      Как заметил rustler2000, удобно иметь дело со слабоструктурироваными данными. Стандартный парсер json полностью работает на интерфейсах. (Хотя сгенерированный код easyjson работает в 5 раз быстрее, он не переживёт изменение структуры сообщения.)
      Интерфейсы очень полезны для статического анализа. Удобно писать с полным автодополнением: если метод не высвечивается после точки, то его нет, и надо искать ошибку выше. Шаблоны С++ не полностью анализируемы, их не всегда с первого раза удаётся скомпилировать.


    1. tmvrus
      27.06.2018 16:03

      Например писать вот такие штуки
      github.com/thedevsaddam/gojsonq


  1. nikitadanilov
    26.06.2018 13:43

    > и хотя во время выполнения значение, хранящееся в переменной интерфейса, может изменять тип, это значение всегда будет удовлетворять интерфейсу. (Никаких undefined, NaN и прочих ломающих логику программы вещей.)

    Во-первых, переводчик улучшил Пайка, добавив предложение в скобках. :-) Во-вторых, переменная интерфейсного типа может хранить nil — значение не удовлетворяющее интерфейсу и ломающее логику.


    1. tmvrus
      26.06.2018 14:52
      +2

      nil — может удовлетворять и не ломать (при определенных условиях)
      play.golang.org/p/dEAFHPb86MM


      1. motakuji
        27.06.2018 08:37

        Потому что в GO это не вызов метода объекта, а вызов функции с передачей структуры в качестве параметра завуалированное синтаксическим сахаром


  1. S_Gonchar
    26.06.2018 15:43
    +1

    Хорошая статья, интересно было почитать! Спасибо автору и переводчику!
    Вот только сегодня проходил эту тему в учебнике D&K, но с ходу ничего не понял (буду перечитывать).


  1. pfihr
    26.06.2018 20:14

    Чтобы понять, достаточно посмотреть на исходный код reflect. В целом суть в том, что на исходное значение создаётся структура с unsafe pointer, и дальше идет работа с ней.