什么是反射
反射就是在程序运行时获得对象的实际类型,从而进行相关的操作。在一些框架中经常使用到反射,需要使用反射来获取传递的数据结构。本文主要讲解一下 go 语言中反射如何使用。
类型和接口
在开始之前,需要先了解一下 go 语言中类型和接口两个概念。go 语言是静态语言,对于每一个变量的实际类型,在编译时就已经确定,例如: int, byte, float。对于 MyType, MyType2 潜在类型都是 int, 两者的区别是在没有类型转换之前是无法进行赋值操作的。
type MyType int
type MyType2 = int
func main() {
var i int
var mt MyType
var mt2 MyType2
mt = MyType(i)
mt2 = i
fmt.Println(i, mt, mt2)
}
在 go 语言有一种特殊的类型叫接口类型,它表示一系列方法签名的集合。接口类型的变量可以存储任何非接口类型的变量,只要这个变量实现了对应接口类型定义的方法即可。我们以 go 语言中的 Reader 类型进行讲解, 自定义类型 MyReader, 实现 Read 接口, 则无论 MyReader 是否包含非接口类型的变量, 均可描述为接口类型 io.Reader。
「思考」为什么要有接口类型的变量呢?
// 定义一个接口类型 Reder, 包含的方法为 func Read(p []byte) (n int, err error)
type Reader interface {
Read(p []byte) (n int, err error)
}
// 自定义类型
type MyReader struct {
str string
}
// 实现接口类型 Reader 的方法
func (myReader *MyReader) Read(p []byte) (n int, err error) {
return 0, nil
}
// 定义接口类型
var myReader io.Reader
// 无论 myReader 的值为什么,其类型均为 io.Reader
myReader = &MyReader{}
在 go 语言中,我们经常看到空接口类型 interface{}, 我们可以将其看作一个不包含任何方法签名的接口类型。不包含任何方法就是包含任何方法,即任何类型都实现了空接口类型的方法,均可以被表示为 interface{}
// 空接口类型,可以描述任何类型
var i interface{}
// 均实现了空接口签名的方法
i = 9
i = myReader
接口的表示
从上节中可以看到,接口类型和普通类型还是存在一定的差别,即只要实现接口中定义的方法就可以被描述为对应的接口类型。在 go 语言中是如何描述接口类型的变量呢? 在接口类型变量中,存储了两个值:分配给该变量的真实值,该变量的真实类型描述「非接口类型描述」。例如, 接口类型变量 myReader, 里面存储了值 temp, 类型 MyReader。
var myReader io.Reader
temp := &MyReader{}
myReader = temp
此外,我们可以让 MyReader 实现 Writer 方法,那么我们就可以通过断言将接口类型变量 myReader 转换成接口类型 io.Writer。对于 myWriter 里面存储的的值为 temp, 类型 MyReader。
func (myReader *MyReader) Write(p []byte) (n int, err error) {
return 0, nil
}
// 类型断言
myWriter := myReader.(io.Writer)
反射的规则
- 反射将接口值转换为反射对象
在 go 语言中,通过reflect.ValueOf(i ayn)
,reflect.TypeOf(i ayn)
分别获取接口变量 i 对应的 value 和 type。对于一个变量, 通过 value 和 type 两个结构就可以对其进行描述。value: 用于表示变量的值、类型等; type: 用于表示变量对应的类型, 其中包括类型的属性、方法、大小等。下面是方法的源码:
// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero Value.
func ValueOf(i any) Value {
if i == nil {
return Value{}
}
// TODO: Maybe allow contents of a Value to live on the stack.
// For now we make the contents always escape to the heap. It
// makes life easier in a few places (see chanrecv/mapassign
// comment below).
escapes(i)
return unpackEface(i)
}
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i any) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}
变量 x 先被转换为空接口类型 any, 然后传入函数 reflect.ValueOf(x), reflect.Value(x) , 将接口类型转化为反射对象 value 和 type。拿到变量的 Value 和 Type 之后,我们就可以拿到了变量的全部信息。
func main() {
s := Student{Name: "zjl", Sex: 1}
v, t := reflect.ValueOf(s), reflect.TypeOf(s)
fmt.Println("type name: ", v.Type().Name())
// 输出 field 信息
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Println("fieldName: ", field.Name, " value: ", v.FieldByName(field.Name).Interface())
}
// 获取函数并执行
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
call := v.MethodByName(m.Name)
fmt.Println(call.Call(nil))
}
}
- 反射可以将反射对象转换为接口对象
既然能将接口变量转换为反射对象,那就需要将反射对象转换为接口对象。例如,将变量 s 转换为反射对象 v, 将 v 转换为接口类型 i, 最后将 i 转换为 Student 对象。
func main() {
s := Student{Name: "zjl", Sex: 1}
v := reflect.ValueOf(s)
i := v.Interface()
vv, ok := i.(Student)
fmt.Println(ok, vv)
}
- 如果要修改反射的对象, 对象必须是可更改的
通常我们需要更新被反射的对象, 这就需要原始对象是可寻址的, 不然就无法进行更新操作。对于不可更新的反射对象, 如果强行更新会导致 panic。
func main() {
s := Student{Name: "zjl", Sex: 1}
v := reflect.ValueOf(s)
f := v.FieldByName("Name")
fmt.Println(f.CanSet()) // false
// f.SetString("xx") panic: reflect: reflect.Value.SetString using unaddressable value
ss := &Student{Name: "zjl", Sex: 1} // 需要使用指针
v = reflect.ValueOf(ss).Elem()
f = v.FieldByName("Name")
f.SetString("xx")
fmt.Println(*ss) // {xx 1}
}
为什么 s 是不可修改的?这里是因为在调用 ValueOf 方法时, 会讲 s 转换为一个局部接口类型 i, i 由两部分组成「value: s 的拷贝, type: 真实类型, Student」。因此, i 是不可寻址的, 无法找到原始的变量 s。因此需要像 ss 一样传入指针。
更新结构体的值
type T struct { // 属性必须是可导出的
Name string
Age int64
}
func main() {
t := &T{Name: "test", Age: 19}
v := reflect.ValueOf(t).Elem()
typeOfT := v.Type()
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}
//0: Name string = test
//1: Age int64 = 19
// 更新结构体的属性
switch f.Kind() {
case reflect.Int64:
f.SetInt(99)
case reflect.String:
f.SetString("a")
default:
continue
}
}
总结
在真实开发的场景, 我们应尽量避免使用反射。因为反射操作会涉及到性能开销。如果使用反射我们需要记住通过反射可以获取 type 和 value 两大对象, 通过这两大对象就能获取所有的信息「类型名、属性、函数等」。如果要修改原对象, 需要保证属性时可修改的。在具体转换过程中, 其实是先将具体类型转换为接口类型, 然后再将接口类型转换反射对象「value」, 最后将 value 转回接口。