75142913在线留言
GO语言学习进阶1:反射Reflect深入理解与分析_Go语言_网络人

GO语言学习进阶1:反射Reflect深入理解与分析

Kwok 发表于:2020-10-19 08:19:14 点击:52 评论: 0

一、反射是什么?

反射是指计算机程序在运行时()可以访问、检测和修改它本身状态或行为的一种能力。GO语言支持一个叫reflect(反射)包,可以实际检测各类数据结构,可以通过reflect包里提供的各类方法获取到程序在运行中(正在进行时Run time)的数据结构的描述及对数据的操作。使用通俗的方式讲,反射可以拿到各种数据在正在运行时(interface{})的属性,如结构体的所有字段及值、结构体的所有方法及调用。GO语言的反射可以操作任意类型的对象。

二、反射可以做什么?

在实际项目开发中,反射主要用于高级的框架开发,使用反射前需要了解变量的内在运行机制,因为反射的功能就是对这些机制的修改与读取。变量的类型信息是一个元信息(类人类的DNA),是GO语言预设的,静态的。变量的值信息是程序运行过程中动态变化的。

我们可以常用reflect里提供的reflect.ValueOf(val)返回一个代表数据的Value(值)。并通过调用reflect.TypeOf(val)来提取动态类型信息,该方法返回一个类型Type。我们可以使用反射获取到函数里参数类型是什么,通过参数判断处理数据。或者是通过用户输入来决定调用哪个函数或者方法,让我们在程序运行期间动态的执行函数。还有很多类似的需要都可以使用反射来完成。

三、反射怎么用?

1、reflect.TypeOf 返回接口中保存的值的类型,TypeOf(nil)会返回nil。

import (
	"fmt"
	"reflect"
)

type student struct {
	name string
}
func main() {
	/*
	   演示 reflect.TypeOf 可以做什么,官方解释:
	   TypeOf返回接口中保存的值的类型,TypeOf(nil)会返回nil。
	*/
	var stu = student{"张三"}
	//使用 reflect.TypeOf 检测类型
	fmt.Println("stu类型是:", reflect.TypeOf(stu))   //stu类型是: main.student
	fmt.Println("123类型是:", reflect.TypeOf(123))   //123类型是: int
	fmt.Println("字符串类型是:", reflect.TypeOf("字符串")) //字符串类型是: string
	fmt.Println("1.23类型是:", reflect.TypeOf(1.23)) //1.23类型是: float64

	//reflect.TypeOf自己的类型是: func(interface {}) reflect.Type
	fmt.Println("reflect.TypeOf自己的类型是:", reflect.TypeOf(reflect.TypeOf))

	//检测一下反射回来的值是什么
	var rTyp reflect.Type = reflect.TypeOf(stu)    //标准定义,平时我们都使用推导rTyp := reflect.TypeOf(stu)
	fmt.Println("rTyp的类型是:", reflect.TypeOf(rTyp)) //rTyp的类型是: *reflect.rtype
}

 TypeOf是最常用的,GO还提供了:PtrTo (t Type) t的指针的类型、SliceOf(t Type) t的切片的类型、MapOf(key, elem Type)返回一个键类型为key,值类型为elem的映射类型(不合法panic)、ChanOf(dir ChanDir, t Type)返回元素类型为t、方向为dir的通道类型。

2、reflect.ValueOf 返回一个初始化为i接口保管的具体值的Value,ValueOf(nil)返回Value零值。 

type student struct {
	name string
}

func main() {
	var stu = student{"张三"}
	//reflect.ValueOf类型是: func(interface {}) reflect.Value
	fmt.Println("reflect.ValueOf类型是:", reflect.TypeOf(reflect.ValueOf))
	var rVal reflect.Value = reflect.ValueOf(stu) //标准定义,实际开发中使用推导rVal := reflect.ValueOf(stu)

	fmt.Println("rVal =", rVal) //rVal = {张三} //亲,这是个假象不信看下面

	//rVal.name undefined (type reflect.Value has no field or method name)
	fmt.Println("rVal里的name =", rVal.name)

	/*
		我们上面使用了标准定义可以看出来rVal是类型是reflect.Value,虽然直接打印就是结构体的数据,
		但是不能通过rVal.name访问里面的数据,如果我们想要把数据转回结构体需要下面2步
	*/
	var iV = rVal.Interface()                 //将rVal数据转成interface()交给iV
	fmt.Println("iV类型是:", reflect.TypeOf(iV)) //iV类型是: main.student,虽然编译器认识你,但还是不能使用哦~
	//fmt.Println("iV里的name =", iV.name)//iV.name undefined ...
	var stu2 = iV.(student)                //必须使用类型断言后才能使用。
	fmt.Println("stu2里的name =", stu2.name) //stu2里的name = 张三
}
/*
批量类型断言参考interface{}里http://www.55mx.com/go/79.html使用的switch v.(type) case
*/

reflect.ValueOf 在读到反射值或者转换的时候会经常使用到。

3、reflect.Value 提供了反射接口及有限使用的方法

 reflect.Value是反射开发中的核心了,官方内置了几十种方法供操作使用,下面的演示代码里只简单说明几个常会用到的,需要更多请参考GO手册。

var stu = student{"张三"}
var rVal = reflect.ValueOf(stu)               //定义一个stu的反射赋予rVal
fmt.Println("rVal类型是:", reflect.TypeOf(rVal)) //rVal类型是: reflect.Value
/*
	使用Value提供的各类常用到的方法
*/
//检测被反射对象的类型
fmt.Println("rVal反射的类型是:", rVal.Type()) //rVal反射的类型是: main.student
fmt.Println("rVal类别是:", rVal.Kind()) //rVal类别是: struct
  1. 类别高于类型,为GO语言定义的基本数据类型,就像上帝定义了动物和植物,我们定义了中国人和外国人。
  2. Kind与Type可能是相同的,num:=1他的type是int,Kind也是int ,取决于被反射的对象。
  3. 结构体的type名总是返回“包名.结构体”而Kind只会返回struct。
  4. Kind返回的值是一组已定义了的常量,可以在手册里看到都是基本的数据类型,如:Bool、Int(16、32、64)Uint(8、16、32、64)、Float32/64、Map、String、Array、Chan...

上面演示代码里已使用到了rVal.Kind(),rVal.Type()方法,下面将演示其它常用方法:

var v int = 10
var rVal = reflect.ValueOf(v)
/*
	知道被反射的类型时我们可以通过对应的方法直接取值使用
	因为我们知道这是int,所以我们可以调用rVal.Int()
*/
fmt.Println("rVal反射的值是:", rVal.Int()) //rVal反射的值是: 10
i := rVal.Int() + 10       //rVal.Int()是一个expression(表达式)不可以和变量相加,但可以和值相加
fmt.Println("i = ", i)     //i =  20

rVal = reflect.ValueOf(&v) //将v的指定地址传给反射

//判断rVal.Elem()是否可修改
if rVal.Elem().CanSet() {
	rVal.Elem().SetInt(200) //通过反射的方法修改v的值
}
fmt.Println("v是:", v) //v是: 200
/*
	Elem返回v持有的接口保管的值的Value封装,或者v持有的指针指向的值的Value封装。
	如果v的Kind不是Interface或Ptr会panic;如果v持有的值为nil,会返回Value零值。
*/

var s string = "网络人"
rVal = reflect.ValueOf(s) //让rVal重新反射一个字符串的类型
fmt.Println("rVal反射的值是:", rVal.String())      //rVal反射的值是: 10
siteInfo := rVal.String() + " www.55mx.com" //rVal.String()返回一个字串并可以正常拼接使用
fmt.Println(siteInfo)                         //网络人 www.55mx.com

/*
	使用Elem()修改string的内容
*/
rVal = reflect.ValueOf(&s)                   //传入s的指针地址
sPtr := rVal.Elem()                          //sPtr得到Elem()返回s持有的接口保管的值的Value封装
fmt.Printf("sPtr类型为:%T,值为:%vn", sPtr, sPtr) //sPtr类型为:reflect.Value,值为:网络人
if sPtr.CanSet() {
	sPtr.SetString("www.55mx.com 网络人")
} else {
	fmt.Print("sPtr(变量s)的值不能修改")
}
fmt.Print(s)//www.55mx.com 网络人

Elem方法应该是理解setint、setstring等方法的难点。可以多看手册和查问资料,慢慢消化吧~其实手册里有讲到传入的值必须是v的持有值。如果v的Kind不是String或者v.CanSet()返回假,会panic。

func (v Value) Elem() Value 这里的v Value就是v.Elem()方法返回的值,即v接口保管的值的Value封装。

4、结构体反射修改字段里的值和调用相关方法

在手册里没有找到类型于一个reflect.Value(v struct).Struct()的方法来访问结构体,所以我们使用结构体的时候必须向上面的代码一样转interface{}后用类型断言。

 

type webSite struct {
	Name string `mytag:"name"`
	Url  string `mytag:"url"`
	Age  int    `mytag:"age"`
}

//首字母要大写哦,要不然Method()是无法访问的
func (w webSite) siteInfo() {
	fmt.Println(w.Name, "(", w.Url, ")运行了", w.Age, "年")
}
func (w webSite) A() {
	fmt.Println("A居然是Method[0]")
}
func (w webSite) B() {
	fmt.Println("B居然是Method[1]")
}
func (w webSite) Z() {
	fmt.Println("Z居然是Method[3]")
}
func main() {
	var 55mx = webSite{"网络人", "www.55mx.com", 10}
	rVal := reflect.ValueOf(55mx)
	typ := reflect.TypeOf(55mx) //反射值的类型,调用tag时候要用
	if rVal.Kind() != reflect.Struct {
		fmt.Print("rVal不是结构体类型")
		return
	}
	//遍历结构体里的所有字段,通过NumField检测长度
	for i := 0; i < rVal.NumField(); i++ {
		fmt.Printf("字段:%d,值为%v,Tag标签为%vn", i, rVal.Field(i), typ.Field(i).Tag.Get("mytag"))
		/*
			字段:0,值为网络人,Tag标签为name
			字段:1,值为www.55mx.com,Tag标签为url
			字段:2,值为10,Tag标签为age
		*/
	}
	//注意TAG是通过typ.Field(i).Tag.Get调用不是rVal.Field哦~

	fmt.Println(typ, "共有", rVal.NumMethod(), "个方法可供调用")
	//main.webSite 共有 3 个方法可供调用,因为有siteInfo首字母没有大写,无法导出

	for i := 0; i < rVal.NumMethod(); i++ {
		//遍历并执行所有的方法
		fmt.Print("正在执行第", i, "个方法:")
		rVal.Method(i).Call(nil)
		/*
			正在执行第0个方法:A居然是Method[0]
			正在执行第1个方法:B居然是Method[1]
			正在执行第2个方法:Z居然是Method[3]
		*/
	}
	rVal = reflect.ValueOf(&55mx)                   //将已实例化的55mx指针地址传进去
	rVal.Elem().Field(0).SetString("美食圈")           //通过反射修改第1个字段Name
	rVal.Elem().FieldByName("Url").SetString("www.meishiq.com") //通过FieldByName修改第Url字段
	rVal.Elem().Field(2).SetInt(8)                    //修改第3个字段Age
	55mx.siteInfo()                                 //美食圈 ( www.meishiq.com )运行了 8 年

}

 Method()里的方法是按字母的ASCII排序的,由A~Z(a~z小字首字母的方法无法使用),并不是我们写方法的顺序,这是注意事项~

Call方法使用输入的参数int调用v持有的函数。例如,如果len(in) == 3,v.Call(in)代表调用v(in[0], in[1], in[2])(其中Value值表示其持有值)。如果v的Kind不是Func会panic。

它返回函数所有输出结果的Value封装的切片。和go代码一样,每一个输入实参的持有值都必须可以直接赋值给函数对应输入参数的类型。如果v持有值是可变参数函数,Call方法会自行创建一个代表可变参数的切片,将对应可变参数的值都拷贝到里面。

Call手册为 func (v Value) Call(in []Value) []Value 传入的参数是一个Value切片,返回的也是Value切片。可以理解一下下面的代码:

var params []reflect.Value                   //声明一个[]reflect.Value切片
params = append(params, reflect.ValueOf(10)) //追加一个reflect.ValueOf[10],将常量10转成reflect.Value
params = append(params, reflect.ValueOf(20))

rVal := reflect.ValueOf(test{}) //rVal反射一个匿名结构体test{}
fmt.Printf("传入的参数params类型为:%Tn", params) //传入的参数params类型为:[]reflect.Value
res := rVal.Method(0).Call(params)        //i + n =  30,这里验证Call是传入的一个reflect切片
fmt.Printf("调用方法后返回的值类型为:%Tn", res)      //调用方法后返回的值类型为:[]reflect.Value
fmt.Println("方法返回的值为:", res[0].Int())     //方法返回的值为: 30,返回的是一个切片值,需要带索引调用方法int,可以什么len(res)检测长度
/*
能看懂上面的代码也基本能理解反射,如果觉得难,代码里就不要写反射了,真的很难阅读的。
*/
  • 反射从接口值到反射对象。
  • 反射从反射对象到接口值。
  • 要修改反射对象,该值必须可设置。

如果不是经验丰富的程序员,我们可以尽量少使用反射,因为反射代码可阅读性差,对性能影响大,由于可直接操作运行中的数据,编译时不能及时发现错误。

除非注明,网络人的文章均为原创,转载请以链接形式标明本文地址:https://www.55mx.com/post/85
标签:reflect反射GOKwok最后编辑于:2020-10-19 21:19:20
0
感谢打赏!

《GO语言学习进阶1:反射Reflect深入理解与分析》的网友评论(0)

本站推荐阅读

热门点击文章