聊一聊 go 的反射 reflect

520 阅读5分钟

什么是反射

反射就是在程序运行时获得对象的实际类型,从而进行相关的操作。在一些框架中经常使用到反射,需要使用反射来获取传递的数据结构。本文主要讲解一下 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 转回接口。