SoFunction
Updated on 2025-03-05

Talk about the three laws of reflection in Go language

Introduction

Reflection is represented in a computer The ability of a program to check its own structure, especially the type. It is a form of metaprogramming and the most confusing part.

Although Go language does not have the concept of inheritance, for the sake of ease of understanding, if a struct A implements all methods of interface B, we call it "inheritance".

Types and interfaces

Reflection is built on a type system, so let's start with the basics of type.

Go is a statically typed language. Each variable has and only one static type, which is determined at compile time. for example int、float32、*MyType、[]byte. If we make the following statement:

type MyInt int

var i int
var j MyInt

In the above code,The type of variable i is int, and the type of j is MyInt. So, although variables i and j have a common underlying type int, their static types are not the same. When directly assigning values ​​to each other without type conversion, the compiler will report an error.

Regarding types, an important category is interface, each interface type represents a fixed collection of methods. An interface variable can store (or "pointing", an interface variable is similar to a pointer) any type of specific value, as long as this value implements all methods of the interface type. A well-known set of examples is and , Reader and Writer types are derived from io packages, and are declared as follows:

// Reader is the interface that wraps the basic Read method.
type Reader interface {
 Read(p []byte) (n int, err error)
}

// Writer is the interface that wraps the basic Write method.
type Writer interface {
 Write(p []byte) (n int, err error)
}

Any implementationRead(Write)We call the type of method inheritance()interface. In other words, a type is The variable can point to any type of variable (interface variables are similar to pointers), as long as this type implements the Read method:

var r 
r = 
r = (r)
r = new()
// and so on

Always remember:No matter what the specific value pointed to by the variable r is, its type is always. Repeat again:Go is a statically typed language, and the static type of the variable r is

A very, very important interface type is an empty interface, i.e.:

interface{}

It represents an empty set without any method. Since any specific value has zero or more methods, the type isinterface{}variables can store any value.

Some people say that Go's interface is dynamic. This statement is wrong! Interface variables are also statically typed, and they always have only one identical static type. If the value it stores changes at runtime, this value must also satisfy the method set of interface types.

Since the relationship between reflection and interface is very close, we must clarify this.

Representation of interface variables

Russ Cox wrote an article in 2009 to introduce the representation of interface variables in Go. Here we do not need to repeat all the details, just make a simple summary.

The Interface variable stores a pair of values: the specific value and descriptor of the value type assigned to the variable. To be more precise, the value is the underlying data that implements the interface, and the type is the description of the underlying data type. For example:

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

In this example, the variable r contains a (value, type) pair on the structure: (tty, ).Notice:Types not only implement the Read method. Although the interface variable only provides call rights to the Read function, the underlying value contains all type information about this value. So we can do type conversion like this:

var w 
w = r.()

The second line of the above code is a type assertion, which determines that the actual value inside the variable r also inherits the interface, so it can be assigned to w. After assignment, w points to the (tty, *) pair, and the variable r points to the same (value, type) pair. No matter how large the method set of the underlying specific values ​​is, due to the static type limitation of the interface, the interface variable can only call some specific methods.

Let's continue to read:

var empty interface{}
empty = w

The empty interface variable empty here also contains (tty, *) pairs. This is easy to understand:An empty interface variable can store any specific value and all description information of that value.

Careful friends may find that no type assertion is used here, because the variable w satisfies all methods of the empty interface (the legendary "no move is better than move"). In the previous example, we take a specific value from Convert to When explicit type assertion is required, it is because Method collection is not subset of .

Another thing to note isThe type in the (value, type) pair must be a specific type (struct or basic type), and cannot be an interface type. Interface types cannot store interface variables.

Let’s introduce this about interfaces. Let’s take a look at the three laws of reflection in Go language.

The first law of reflection: reflection can convert "interface type variable" to "reflective type object".

Note: The reflection type here refers to and

In terms of usage, reflection provides a mechanism that allows a program to check the (value, type) pairs stored inside the interface variable at runtime. At the beginning, let's first understand the two types of reflect packages:Type and Value. These two types make it possible to access data within the interface. They correspond to two simple methods, namely and, used to read interface variables respectively and part. Of course, from Easy to obtain. For now we will separate them first.

First, let's take a look:

package main

import (
 "fmt"
 "reflect"
)

func main() {
 var x float64 = 3.4
 ("type:", (x))
}

This code will print out:

type: float64

You may wonder: Why don’t you see the interface? This code looks like it just passes a variable x of type float64 to , there is no passing interface. In fact, the interface is there. Check the TypeOf documentation and you will find that the function signature contains an empty interface:

// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type

We call (x)When , x is stored in an empty interface variable and passed over; then the empty interface variable is disassembled to restore its type information.

function The underlying value will also be restored (here we ignore the details and only focus on executable code):

var x float64 = 3.4
("value:", (x))

The above code prints out:

value: <float64 Value>

typeandThere are many ways we can check and use them. Here we give a few examples. typeThere is a method Type(), which returns a Object of type. Type and Value both have a method called Kind, which will return a constant representing the type of the underlying data. Common values ​​include:Uint、Float64、Slicewait. The Value type also has some methods similar to Int and Float, which are used to extract the underlying data. The Int method is used to extract int64, and the Float method is used to extract float64. Refer to the following code:

var x float64 = 3.4
v := (x)
("type:", ())
("kind is float64:", () == reflect.Float64)
("value:", ())

The above code will print out:

type: float64
kind is float64: true
value: 3.4

There are also some methods used to modify data, such as SetInt and SetFloat. Before discussing them, we must first understand "settability", which will be explained in detail in the "Third Law of Reflection".

The reflection library provides many attributes worth listing and discussing separately. First, let’s introduce the getter and setter methods of Value. In order to ensure the simplification of the API, these two methods operate on the one with the largest range of types in a certain group. For example, int64 is used when dealing with any integer number with symbols. In other words, the return value of the Int method of type Value is int64, and the parameter type received by the SetInt method is also int64. When used in practice, it may need to be converted to the actual type:

var x uint8 = 'x'
v := (x)
("type:", ())       // uint8.
("kind is uint8: ", () == reflect.Uint8) // true.
x = uint8(())    //  returns a uint64.

The second property is the Kind method of the reflective type variable (reflection object) that returns the type of the underlying data, rather than the static type. If a reflection type object contains a user-defined integer, see the code:

type MyInt int
var x MyInt = 7
v := (x)

In the above code, although the static type of variable v is MyInt, not int, the Kind method still returns . In other words, the Kind method does not distinguish between MyInt and int like the Type method.

The second law of reflection: reflection can convert "reflection type object" to "interface type variable".

Similar to reflection in physics, reflection in Go can also create its own reverse type objects.

Based on a variable of type, we can use the Interface method to restore the value of its interface type. In fact, this method will package the type and value information and populate it into an interface variable and then return it. Its function declaration is as follows:

// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}

Then, we can restore the underlying specific value by asserting:

y := ().(float64) // y will have type float64.
(y)

The above code will print out a float64 value, which is the value represented by the reflection type variable v.

In fact, we can better utilize this feature. In the standard libraryand All functions receive empty interface variables as parameters, and the interface variables will be unpacked within the fmt package (in the previous example, we did similar operations). Therefore, when the print function of the fmt package prints data of type variables, it only needs to pass the result of the Interface method to the formatted print program:

(())

You might ask: What do you not print v directly, such as (v)? The answer is that the type of v is , and what we need is the specific value it stores. Since the underlying value is a float64, we can print in format:

("value is %7.1e\n", ())

The print result of the above code is:

3.4e+00

Again, this time, there is no need to be correct () The result of the type assertion is performed. The empty interface value contains the type information of the specific value, and the Printf function will restore the type information.

Simply put,Interface Methods andValueOf The function does exactly the opposite, the only thing is that the static type of the return value is interface{}。

Let's re-explain:Go's reflection mechanism can convert "interface type variable" to "reflective type object", and then convert "reflective type object" over.

The third law of reflection: If you want to modify a "reflective type object", its value must be "writable".

This law is subtle and easily confusing. But if you start with the first law, it should be easier to understand.

The following code doesn't work properly, but it's very worth studying:

var x float64 = 3.4
v := (x)
(7.1) // Error: will panic.

If you run this code, it throws a weird exception:

panic:  using unaddressable value

The problem here is not that the value 7.1 cannot be addressed, but because the variable v is "not writable". "Writability" is an attribute of a reflective type variable, but not all reflective type variables have this attribute.

We can check the "writability" of a type variable through the CanSet method. For the above example, you can write it like this:

var x float64 = 3.4
v := (x)
("settability of v:", ())

The above code prints:

settability of v: false

For a Value type variable that does not have "writability", calling the Set method will report an error. First, we need to figure out what "writability" is.

"Writability" is somewhat similar to addressing capabilities, but is more stringent. It is an attribute of a reflective type variable, giving the variable the ability to modify the underlying storage data. "Writability" is ultimately determined by the fact that the reflective object stores the original value. Let's give a code example:

var x float64 = 3.4
v := (x)

Here we pass it to The function is a copy of the variable x, not the x itself. Imagine if the following line of code can be executed successfully:

(7.1)

The answer is:If this line of code is executed successfully, it does not update x , although it seems that the variable v is created based on x . Instead, it updates a copy of x that exists inside the reflective object v, while the variable x itself is completely unaffected. This can cause confusion and has no meaning, so it is illegal. "Writability" is designed to avoid this problem.

This looks weird, it is not, and similar situations are common. Consider the following line of code:

f(x)

In the above code, we pass a copy of the variable x to the function, so we do not expect it to change the value of x. If the expected function f can be modified to change the variable x, we must pass the address of x (i.e., a pointer to x) to the function f, as follows:

f(&x)

You should be very familiar with this line of code, and the reflection works the same. If you want to change the value x through reflection modification, just pass the pointer of the variable you want to modify to the reflection library.

First, initialize the variable x as usual, and then create a reflective object pointing to it, named p:

var x float64 = 3.4
p := (&x) // Note: take the address of x.
("type of p:", ())
("settability of p:", ())

The output of this code is:

type of p: *float64
settability of p: false

The reflective object p is not writable, but we are not like modifying p. In fact, what we want to modify is *p. In order to obtain the data pointed to by p, the Elem method of type Value can be called. The Elem method is able to "dereference" the pointer and then store the result in a reflective Value type object v:

v := ()
("settability of v:", ())

In the above code, the variable v is a writable reflection object, and the code output also verifies this:

settability of v: true

Since the variable v represents x, we can modify the value of x using:

(7.1)
(())
(x)

The output of the above code is as follows:

7.1
7.1

Reflection is not easy to understand.andIt will confuse the execution of the program, but what it does is exactly what the programming language does. You just need to remember: as long as the reflective objects want to modify the objects they represent, you must get the address of the objects they represent.

Struct

In the previous example, the variable v is not a pointer itself, it is just derived from a pointer. When applying reflection to a structure, a common way is to use reflection to modify certain fields of a structure. As long as we have the address of the structure, we can modify its fields.

The following is a simple example to analyze the structure type variable t.

First, we create a reflection type object, which contains a pointer to a structure, as it will be modified later.

Then we set typeOfT to its type and iterate through all the fields.

Notice:We extract the name of each field from the struct type, but each field itself is a regular object.

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

The output of the above code is as follows:

0: A int = 23
1: B string = skidoo

There is another point to point out here: the fields of the variable T are all capitalized in the first letter (exposed to the outside), because only fields exposed to the outside in the struct are "writable".

Since the variable s contains a "writable" reflective object, we can modify the fields of the structure:

())(0).SetInt(77)
(1).SetString("Sunset Strip")
("t is now", t)

The output of the above code is as follows:

t is now {77 Sunset Strip}

If the variable s is created with t , not &t , calling SetInt and SetString will fail because the field of t is not "writable".

in conclusion

Finally, repeat the three laws of reflection again:

1. Reflection can convert "interface type variable" to "reflection type object".

2. Reflection can convert "reflection type object" to "interface type variable".

3. If you want to modify the "reflective type object", its value must be "writable".

Once you understand these laws, using reflection will be a very simple thing. It is a powerful tool, and you must use it with caution when using it, and not abuse it.

There is still a lot we have not discussed about reflection, including pipeline-based sending and receiving, memory allocation, use of slices and maps, calling methods and functions, which we will introduce in subsequent articles. Please continue to follow me.

Original author Rob Pike, translated by Oscar