Hi, Habr! I present to you the translation of the article
"The Laws of Reflection" from the creator of the language.
Reflection is the ability of a program to explore its own structure, especially through types. This is a form of metaprogramming and a great source of confusion.
In Go, reflection is widely used, for example, in the test and fmt packages. In this article we will try to get rid of the "magic", explaining how reflection works in Go.
Types and Interfaces
Since reflection is based on the type system, let's refresh the knowledge about types in Go.
Go is statically typed. Each variable has one and only one static type, fixed at compile time:
int, float32, *MyType, []byte
... If we declare:
type MyInt int var i int var j MyInt
then
i
is of type
int
and
j
is of type
MyInt
. The variables
i
and
j
have different static types and, although they have the same basic type, they cannot be assigned to each other without conversion.
One of the important categories of type are interfaces, which are fixed sets of methods. An interface can store any specific (non-interface) value as long as that value implements the methods of the interface. A famous pair of examples is
io.Reader and io.Writer , the Reader and Writer types from the
io package :
It is said that any type that implements the
Read()
or
Write()
method with this signature implements
io.Reader
or
io.Writer
respectively. This means that a variable of type
io.Reader
can contain any value whose type has the Read () method:
var r io.Reader r = os.Stdin r = bufio.NewReader(r) r = new(bytes.Buffer)
It is important to understand that
r
can be assigned any value that implements
io.Reader
. Go is statically typed, and the static type
r
is
io.Reader
.
An extremely important example of an interface type is the empty interface:
interface{}
It is an empty set of ∅ methods and is implemented by any value.
Some say that Go interfaces are dynamic, with dynamic typing, but this is misleading. They are statically typed: a variable with an interface type always has the same static type, and although at run time the value stored in the interface variable can change the type, this value will always satisfy the interface. (No
undefined
,
NaN
and other things breaking the logic of the program of things.)
This must be understood - reflection and interfaces are closely related.
Internal interface view
Russ Cox wrote a detailed
blog post about the interface in Go. Not less good article
is on Habr'e . There is no need to repeat the whole story, the main points are mentioned.
An interface type variable stores a pair: the specific value assigned to the variable, and the type descriptor of that value. More precisely, the value is the base data element that implements the interface, and the type describes the complete type of this element. For example, after
var r io.Reader tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) if err != nil { return nil, err } r = tty
r
contains, schematically, a pair
(, ) --> (tty, *os.File)
. Note that the type
*os.File
implements methods other than
Read()
; even if the interface value provides access only to the Read () method, the value inside carries all the information about the type of this value. That's why we can do these things:
var w io.Writer w = r.(io.Writer)
The expression in this assignment is a type statement; it claims that the element inside
r
also implements
io.Writer
, and therefore we can assign it to
w
. After assignment,
w
will contain a pair
(tty, *os.File)
. This is the same pair as in
r
. The static interface type determines which methods can be called on an interface variable, although a specific set of methods may have a specific value inside.
Continuing, we can do the following:
var empty interface{} empty = w
and the empty value of the empty field will again contain the same pair
(tty, *os.File)
. This is convenient: a blank interface can contain any value and all the information that we will ever need from it.
We do not need a type statement here because it is known that
w
satisfies an empty interface. In the example where we transferred the value from the
Reader
to
Writer
, we had to explicitly use a type statement, because the
Writer
methods are not a subset of the
Reader
. Attempting to convert a value that does not match the interface will cause panic.
One important detail is that a pair within an interface always has a form (value, specific type) and cannot have a form (value, interface). Interfaces do not support interfaces as values.
Now we are ready to study reflect.
The first law of reflection reflect
- Reflection extends from the interface to the reflection of the object.
At a basic level, reflect is just a mechanism for learning the type and value pairs stored inside an interface variable. To get started, there are two types that we need to know about:
reflect.Type
and
reflect.Value
. These two types provide access to the contents of the interface variable and are returned by simple functions, reflect.TypeOf () and reflect.ValueOf (), respectively. They extract parts from the interface value. (In addition, from
reflect.Value
easy to get
reflect.Type
, but let's not mix the concepts of
Value
and
Type
at the moment.)
Let's start with
TypeOf()
:
package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.4 fmt.Println("type:", reflect.TypeOf(x)) }
The program will output
type: float64
The program is similar to passing a simple
float64 x
variable to
reflect.TypeOf()
. Do you see the interface? And it is -
reflect.TypeOf()
takes an empty interface, according to the function declaration:
When we call
reflect.TypeOf(x)
,
x
first stored in an empty interface, which is then passed as an argument;
reflect.TypeOf()
unpacks this empty interface to restore type information.
The function
reflect.ValueOf()
, of course, restores the value (we will ignore the template and focus on the code):
var x float64 = 3.4 fmt.Println("value:", reflect.ValueOf(x).String())
will print
value: <float64 Value>
(We call the
String()
method explicitly, because by default the fmt package decompresses to
reflect.Value
and outputs a specific value.)
Both
reflect.Type
and
reflect.Value
have many methods that allow you to explore and modify them. One important example is that
reflect.Value
has a
Type()
method that returns the value type.
reflect.Type
and
reflect.Value
have a
Kind()
method that returns a constant indicating which primitive element is stored:
Uint, Float64, Slice
... These constants are declared in the enumeration in the package reflect. The
Value
methods with names such as
Int()
and
Float()
allow us to pull out the values (like int64 and float64) contained within:
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())
will print
type: float64 kind is float64: true value: 3.4
There are also methods such as
SetInt()
and
SetFloat()
, but to use them we need to understand setability, the theme of the third law of reflection.
The reflect library has a couple of properties that need to be highlighted. First, to keep the API simple, the “getter” and “setter”
Value
methods act on the largest type that can contain the value:
int64
for all
int64
integers. That is, the
Int()
method of
Value
returns
int64
, and the value of
SetInt()
takes
int64
; may need to be converted to actual type:
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())
will be
type: uint8 kind is uint8: true
Here
v.Uint()
returns
uint64
, an explicit type statement is necessary.
The second property is that the object's
Kind()
reflect describes the base type, not the static type. If the reflection object contains a value of a user-defined integer type, as in
type MyInt int var x MyInt = 7 v := reflect.ValueOf(x)
v.Kind() == reflect.Int
, although the static type
x
is
MyInt
, not
int
. In other words,
Kind()
cannot distinguish between
int
from
MyInt
, as opposed to
Type()
.
Kind
can only accept values of built-in types.
The second law of reflection
- Reflection extends from the reflect object to the interface.
Like physical reflection, reflect in Go creates its opposite.
Having
reflect.Value
, we can restore the value of the interface using the
Interface()
method; The method wraps the type and value information back into the interface and returns the result:
bvt
As an example:
y := v.Interface().(float64)
prints the value of
float64
represented by the reflect object
v
.
However, we can do even better. The arguments to
fmt.Println()
and
fmt.Printf()
are passed as empty interfaces, which are then unpacked with the fmt package inside, as in the previous examples. Therefore, all that is required to print the contents of
reflect.Value
correctly is to pass the result of the
Interface()
method to the formatted output function:
fmt.Println(v.Interface())
(Why not
fmt.Println(v)
? Because
v
is of type
reflect.Value
; we want to get the value contained inside.) Since our value is
float64
, we can even use the floating-point format if we want:
fmt.Printf("value is %7.1e\n", v.Interface())
will output in a particular case
3.4e+00
Again, there is no need to
v.Interface()
result type
v.Interface()
to
float64
; an empty interface value contains information about a specific value inside, and
fmt.Printf()
restore it.
In short, the
Interface()
method is the inverse of the
ValueOf()
function, except that its result is always of the static type
interface{}
.
Repeat: Reflection extends from the interface values to the objects of reflection and back.
The third law of reflection reflection
- To change the reflection object, the value must be adjustable.
The third law is the most subtle and confusing. Starting from first principles.
Here such code does not work, but deserves attention.
var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1)
If you run this code, it will crash with a panic with a critical message:
panic: reflect.Value.SetFloat
The problem is not that the
7.1
literal is not addressed; this is what
v
not installable. Installability is the
reflect.Value
property, and not every
reflect.Value
has it.
The method
reflect.Value.CanSet()
reports the installability of
Value
; in our case:
var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("settability of v:", v.CanSet())
will print:
settability of v: false
Error calling
Set()
method on unsettable value. But what is setability?
Installability is a bit like addressability, but stricter. This property, at which the reflection object can change the stored value that was used when creating the reflection object. Installability is determined by whether the reflection object contains the original element, or only its copy. When we write:
var x float64 = 3.4 v := reflect.ValueOf(x)
we pass a copy of
x
to
reflect.ValueOf()
, so the interface is created as an argument for
reflect.ValueOf()
is a copy of
x
, not
x
itself. Thus, if the statement:
v.SetFloat(7.1)
would be executed, it would not update
x
, although
v
looks like it was created from
x
. Instead, he would update a copy of
x
stored inside the value of
v
, and the
x
itself would not be affected. This is forbidden, so as not to cause problems, and setability is a property used to prevent a problem.
This should not seem strange. This is a common situation in unusual clothes. Consider passing
x
to a function:
f(x)
We do not expect
f()
to change
x
, because we passed a copy of the value of
x
, not
x
itself. If we want
f()
directly change
x
, we must pass a pointer to
x
to our function:
f(&x)
This is straightforward and familiar, and reflection works similarly. If we want to change
x
using reflection, we must provide the reflection library with a pointer to the value we want to change.
Let's do that. First we initialize
x
as usual, and then create a
reflect.Value p
that points to it.
var x float64 = 3.4 p := reflect.ValueOf(&x)
will lead
type of p: *float64
settability of p: false
The reflection object
p
cannot be set, but this is not the
p
we want to set, it is a
*p
pointer. To get what
p
points to, we call the
Value.Elem()
method, which takes the value indirectly through the pointer, and store the result in
reflect.Value v
:
v := p.Elem() fmt.Println("settability of v:", v.CanSet())
Now
v
is an installable object, the result is:
settability of v: true
and since it represents
x
, we can finally use
v.SetFloat()
to change the value of
x
:
v.SetFloat(7.1) fmt.Println(v.Interface()) fmt.Println(x)
output as expected
7.1
7.1
Reflect can be difficult to understand, but it does exactly what the language does, albeit with
reflect.Type
and
reflection.Value
, which can hide what is happening. Just keep in mind that
reflection.Value
needs the address of a variable to change it.
Structures
In our previous example,
v
not a pointer, it was simply derived from it. A common way to create this situation is to use reflection to change the structure fields. As long as we have the address of the structure, we can change its fields.
Here is a simple example that analyzes the value of structure
t
. We create a reflection object with the address of the structure to change it later. Then we set typeOfT to its type and iterate over the fields using simple method calls (see.
Detailed description of the package ). Note that we extract the field names from the structure type, but the fields themselves are normal
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()) }
The program will output
0: A int = 23
1: B string = skidoo
There is another item about installability: the names of
T
fields in uppercase (exportable), because only exported fields are settable.
Since
s
contains a settable reflection object, we can change the structure field.
s.Field(0).SetInt(77) s.Field(1).SetString("Sunset Strip") fmt.Println("t is now", t)
Result:
t is now {77 Sunset Strip}
If we change the program so that
s
created from
t
rather than
&t
, the
SetInt()
and
SetString()
calls would end in panic, since the
t
fields would not be settable.
Conclusion
Recall the laws of reflection:
- Reflection extends from the interface to the reflection of the object.
- Reflection extends from the reflection of the object to the interface.
- To change the reflection object, the value must be settable.
Posted by
rob pike