Laws of reflection in Go

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 :

 // Reader -  ,    Read(). type Reader interface { Read(p []byte) (n int, err error) } // Writer -  ,    Write(). type Writer interface { Write(p []byte) (n int, err error) } 

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



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:

 // TypeOf()  reflect.Type    . func TypeOf(i interface{}) Type 

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()) // v.Uint  uint64. 

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   Value. 

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



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:

 // Interface   v  interface{}. func (v Value) Interface() interface{} 
bvt
As an example:

 y := v.Interface().(float64) // y   float64. fmt.Println(y) 

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



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) //   x. fmt.Println("type of p:", p.Type()) fmt.Println("settability of p:", p.CanSet()) 

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:


Posted by rob pike

Source: https://habr.com/ru/post/415171/


All Articles