golang 反射获取成员方法时遇到的坑

​ 今天在学习golang的反射时遇到了个坑,百度上又搜不到对这个坑的详细描述。既然如此,那就自己写吧╮(╯▽╰)╭。

一、出现过程

首先我们先定义一个测试用的结构体,并且该结构体拥有两个方法,分别为Test1() 和Test2(),需要注意一下,这个结构体的方法没有使用指针,用的是值拷贝。并且注意结构体首字母要大写,不然反射无法访问。

type Test struct {
	
}

// 结构体使用值拷贝
// 注意这里结构体首字母要大写,不然反射无法访问
func (test Test) Test1()  {
	fmt.Println("Test1() Hello World! ")
}

// 结构体使用值拷贝// 结构体使用值拷贝
// 注意这里结构体首字母要大写,不然反射无法访问
func (test Test) Test2(name string)  {
	fmt.Println("Test2() Hello World! %s \n", name)
}

通过打印成员方法的数量来看结果

test1 := Test{}
// 获取类型信息
//rTest1 := reflect.TypeOf(test1)
// 获取值信息
rTest2 := reflect.ValueOf(test1)
// 打印成员方法数量
fmt.Println("成员方法的数量:", rTest2.NumMethod())

可以看到,正常打印了结构体中的方法数量

C:\Users\AppData\Local\Temp\GoLand\___go_build_demo02_go.exe
成员方法的数量: 2

Process finished with the exit code 0

现在修改成员方法,改用指针的形式

type Test struct {
}

func (test *Test) Test1() {
   fmt.Println("Test1() Hello World! ")
}

func (test *Test) Test2(name string) {
   fmt.Println("Test2() Hello World! %s \n", name)
}

再次打印成员变量

C:\Users\AppData\Local\Temp\GoLand\___go_build_demo02_go.exe
成员方法的数量: 0

Process finished with the exit code 0

可以看到。修改为指针后成员方法全都获取不到了,此时我们可以通过修改结构体的定义的时候使用指针类型,或者通过直接&符号获取当前实例化结构体的指针对象,可以解决以上情况。

// 修改结构体定义时使用指针类型
test := &Test{}
// 获取反射的类型
//rType := reflect.TypeOf(i)
// 获取反射的实例
rVal := reflect.ValueOf(test)

// 打印成员方法数量
fmt.Println("成员方法的数量:", rVal.NumMethod())
test := Test{}
// 获取反射的类型
// rType := reflect.TypeOf(i)
// 获取反射的实例
// 通过&符号将test地址传入valueof
rVal := reflect.ValueOf(&test)

// 打印成员方法数量
fmt.Println("成员方法的数量:", rVal.NumMethod())

二、实战扩展

在项目中使用反射都是会封装成一个func中进行调用。在func中调用又有些不同,接下来例子会对如何在func使用进行详细的介绍。下方是进行测试的代码

type Test struct {
}

func (test *Test) Test1() {
   fmt.Println("Test1() Hello World! ")
}

func (test *Test) Test2(name string) {
   fmt.Println("Test2() Hello World! %s \n", name)
}

func main() {
   test := Test{}
   reflectTest(&test)

}

func reflectTest(i interface{}) {

   rVal := reflect.ValueOf(&i)
   // 打印成员方法数量
   fmt.Println("成员方法的数量:", rVal.Elem().Elem().NumMethod())
}

这部分是运行的主函数,因为结构体的成员方法使用的是指针类型,所以需要将test的地址传入

func main() {
   test := Test{}
   reflectTest(&test)
}

形参使用空接口类型,go中所有的数据类型均实现了空接口类型,所以该func可以接收所有类型的参数

func reflectTest(i interface{}) {
   
   rVal := reflect.ValueOf(&i)
   // 打印成员方法数量
   fmt.Println("成员方法的数量:", rVal.NumMethod())
}

通过结果可以看到,使用之前的方法打印出来的成员方法数量为0,接下来就解释一下为什么会这样以及如果获取到具体的方法。

C:\Users\AppData\Local\Temp\GoLand\___go_build_demo02_go.exe
成员方法的数量: 0

Process finished with the exit code 0

通过打印地址,可以看到 i接口的地址与 test对象的地址并不同,但 i接口中存的值与test对象的地址相同。rVal := reflect.ValueOf(&i),这句代码中&i传入的是i接口的地址,并不是test对象的地址。所以获取到的成员方法为0

C:\Users\AppData\Local\Temp\GoLand\___go_build_demo02_go.exe
test对象的地址:0xc2a560
i接口的地址 0xc000088220
i接口的值 0xc2a560

Process finished with the exit code 0

为了解决这种情况,需要使用value结构体的Elem()方法。方法注释粘贴自 Golang标准库中文文档

func (Value) Elem

func (v Value) Elem() Value

​ Elem返回v持有的接口保管的值的Value封装,或者v持有的指针指向的值的Value封装。如果v的Kind不是Interface或Ptr会panic;如果v持有的值为nil,会返回Value零值。

简单来说,这个方法会帮我们通过指针去获取具体对象的值。

根据上文的分析,可以得到如下结论。首先通过 Elem()获取 i 接口的具体值,也就是 test对象的地址。最后再使用一次Elem()获取E到test具体对象的值

改进后代码如下图所示

func reflectTest(i interface{}) {
   rVal := reflect.ValueOf(&i)
   // 获取 i接口的值(test对象的地址)
   rVal = rVal.Elem()
   // 通过 test对象的地址获取test对象
   rVal = rVal.Elem()
   // 打印成员方法数量
   fmt.Println("成员方法的数量:", rVal.NumMethod())
}

运行结果如下所示

C:\Users\AppData\Local\Temp\GoLand\___go_build_demo02_go.exe
成员方法的数量: 2

Process finished with the exit code 0

获取到具体的成员方法,就可以通过Call()来运行函数。

func (Value) Call

func (v Value) Call(in []Value) []Value

​ Call方法使用输入的参数in调用v持有的函数。例如,如果len(in) == 3,v.Call(in)代表调用v(in[0], in[1], in[2])(其中Value值表示其持有值)。如果v的Kind不是Func会panic。它返回函数所有输出结果的Value封装的切片。和go代码一样,每一个输入实参的持有值都必须可以直接赋值给函数对应输入参数的类型。如果v持有值是可变参数函数,Call方法会自行创建一个代表可变参数的切片,将对应可变参数的值都拷贝到里面。

成员方法运行代码:

func reflectTest(i interface{}) {
   rVal := reflect.ValueOf(&i)
   // 获取 i接口的值(test对象的地址)
   rVal = rVal.Elem()
   // 通过 test对象的地址获取test对象
   rVal = rVal.Elem()
   // 打印成员方法数量
   //fmt.Println("成员方法的数量:", rVal.NumMethod())

   // 通过方法名称
   test1Method := rVal.MethodByName("Test1")
   test2Method := rVal.MethodByName("Test2")
   
   // 无参
   test1Method.Call(nil)
   
   // 有参
   // 有参需要创建一个 reflect.Value的切片,然后按照形参顺序为切片添加参数
   test2Param := []reflect.Value{reflect.ValueOf("张三")}
   test2Method.Call(test2Param)
}

运行结果:

C:\Users\AppData\Local\Temp\GoLand\___go_build_demo02_go.exe
Test1() Hello World!
Test2() Hello World! 张三 

Process finished with the exit code 0