Go核心开发学习笔记(廿九) —— 反射

反射使用的地方

  1. 序列化和反序列化时,如果希望序列时将结构体字段名称大写转换成小写,json:"xxx" 这里就用到了反射。
  2. 两个匿名函数变量,定义一个适配器函数用作统一处理接口:
    适配器函数:假设匿名函数名字为<匿名函数名称>,匿名函数中参数为a,b… 则适配器函数为 func(<匿名函数名称>,a,b…)
    就是说建立一个模板,匿名函数函数名称和匿名函数中的参数都作为适配器函数的参数传递。
  3. 反射价值在于自己可以开发go框架。

反射原理

  1. 反射可以在运行时动态获取变量的各种信息,比如变量的类型type,类别kind。
    类型和类别如果都是普通数据类型,没有区别;
    如果是结构体变量,类型和类别是有区别的;

  2. 通过反射,可以修改变量的值,可以调用关联的方法。

  3. 使用反射需要 import "reflect"

反射应用

  1. 常用的两个函数: reflect.TypeOf(<变量>) ; reflect.ValueOf(<变量>)

  2. 使用案例说明,案例后面均有注释说明。

    演示1: 基本数据类型 ≒ interface{} ≒ reflect.Value

    package main
    import (
    	"fmt"
    	"reflect"
    )
    func main() {
    	var num int = 100
    	/*
    	问题一:
    	通过反射拿到num的rtype和rvalue
    	注意区分反射拿到的值前面加个r做区分,reflect.rtype和type不是同一个类型
    	这些*reflect.rtype 或 reflect.Value 是有方法的,和普通数据类型是不一样的。
    	*/
    	rty := reflect.TypeOf(num)           // int 注意这个int不是普通数据类型int,是有很多方法的
    	rval := reflect.ValueOf(num)         // 100 注意这个100不是普通值为100,是有很多方法的
    	fmt.Printf("%T %v\n",rty,rty)   //*reflect.rtype
    	fmt.Printf("%T %v\n",rval,rval)  // reflect.Value
    
    	/*问题二:
    	举例说明,rval如果是int类型,那么可以加减乘除,下面验证
    	 */
    	n1 := 10
    	n2 := 2 + n1
    	fmt.Println(n2)
    	//fmt.Println(n2 + rval)  //invalid operation: n2 + rval (mismatched types int and reflect.Value)
    	/*
    	如何解决上述问题
    	func (v Value) IsNil() bool
    	func (v Value) Kind() Kind
    	func (v Value) Type() Type
    	func (v Value) Convert(t Type) Value
    	func (v Value) Elem() Value
    	func (v Value) Bool() bool
    	func (v Value) Int() int64
    	 */
    	fmt.Println(int64(n2) + rval.Int())  //提供将reflect.Value转化为int64的方法,这样强转一下就好了
    
    	/*
    	问题三:
    	如何将reflect.Value转换为最初的int:只需要将reflect.Value转为空接口,再把空接口断言成需要转换的数据类型即可
    	 */
    	num_ori := rval.Interface().(int)
    	fmt.Printf("%T %v\n",num_ori,num_ori)  //int 100
    }
    

    演示2: 结构体类型 ≒ interface{} ≒ reflect.Value

    package main
    import (
    	"fmt"
    	"reflect"
    )
    type Kunkun struct {
    	Name string
    	Age int
    	Skill string
    }
    func main() {
    	/*
    	定义一个结构体实例caixukun,通过反射拿到type和value
    	 */
    	var caixukun = Kunkun{"蔡徐坤",20,"Ctrl"}
    	cxkrty := reflect.TypeOf(caixukun)
    	cxkrval := reflect.ValueOf(caixukun)
    	fmt.Printf("%T %v\n",cxkrty,cxkrty)   // *reflect.rtype  main.Kunkun
    	fmt.Printf("%T %v\n",cxkrval,cxkrval)  // reflect.Value {蔡徐坤 20 Ctrl} 仅在运行时知道类型,编译阶段过不去
    
    	//将拿到的值转换成空接口,如果这是想取cxkival中的字段值是无法取出来的
    	cxkival := cxkrval.Interface()
    	fmt.Printf("%T %v\n",cxkival,cxkival) //main.Kunkun {蔡徐坤 20 Ctrl}
    	fmt.Println(caixukun.Name)
    	//fmt.Println(cxkival.Name)  //cxkival.Name undefined (type interface {} is interface with no methods)
    
    	//将空接口类型断言成结构体类型就可以使用字段了
    	cxk_ori, ok := cxkival.(Kunkun)  //已经将空接口类型数据断言成Kunkun结构体
    	if ok {
    		fmt.Println("成功断言取出字段名称: ",cxk_ori.Name)    //蔡徐坤
    	}
    }
    

反射注意事项和细节

  1. reflect.Value.Kind(),reflect.rType.Kind() 获取的是变量的类别,返回的是一个常量,区别于类型而言,类别是类型的集合。

    var num int = 10    // type: int , Kind: int  这是普通数据类型
    var cxk Kunkun      // type: Kunkun , Kind: Struct 结构体就有宏观和微观概念了,例如Kunkun是自定义type,是结构体的一类,Struct包含Kunkun这种结构体。
    
  2. 使用反射的方式获取变量的值,要求数据类型匹配,否则会panic

  3. 通过反射来修改变量的值,见下面两个演示:

    演示1:通过反射改变普通变量的值

    package main
    
    import (
    	"fmt"
    	"reflect"
    )
    func main() {
    	/*
    	1. 通过反射修改普通变量的值
    	2. 通过反射修改结构体实例的值
    	 */
    	//var cxk = Kunkun{"蔡徐坤",20,"Ctrl"}
    	var num int = 100
    	//使用反射修改普通变量的值,如果要改变变量值,必须传递地址,通过改变内存地址中的值完成变量改变
    	rNum := reflect.ValueOf(num)
    	fmt.Printf("%T %v\n",rNum,rNum)
    	//rNum.SetInt(20)            	   //reflect.Value.SetInt using unaddressable value,因为不是地址格式的数据所以不行
    	//rNum.Elem().SetInt(20)          //把num变成20
    	fmt.Printf("%T %v\n",num,num)   //这样改变肯定无效,因为不是指针格式
    
    	//下面为正确做法
    	rNum1 := reflect.ValueOf(&num)   //必须是地址,不然没有意义,无法改变num的值
    	fmt.Printf("%T %v\n",rNum1,rNum1)
    	rNum1.Elem().SetInt(20)          //把num变成20,rNum1为地址,rNum1.Elem()为一个value,Elem()用的最多,一定要注意
    	fmt.Printf("%T %v\n",num,num)    //如果没有Elem(),unaddressable value panic
    }
    

    演示2:通过反射改变结构体字段的值

    package main
    
    import (
    	"fmt"
    	"reflect"
    )
    
    type kunkun struct {
    	Name string
    	Age int
    	Skill string
    }
    
    func main() {
    	/*
    	上面理论讲的很清楚了,下方与上方修改普通变量几乎没有区别,唯一在于传入的地址变成了字段 &(cxk.Skill)
    	 */
    	var cxk = kunkun{"蔡徐坤",20,"Ctrl"}
    	rCxk := reflect.ValueOf(&(cxk.Skill))
    	rCxk.Elem().SetString("唱跳rap篮球")      //相当于改变了原来结构体实例中的技能变为 "唱跳rap篮球"
    	fmt.Println(cxk.Skill)
    }
    

★★★反射的最佳实践

案例:使用反射来遍历结构体字段,调用结构体的方法,并获得结构体的标签值。

package main

import (
	"fmt"
	"reflect"
)

//kunkun结构体
type caixukun struct {
	Name string   `json:"name"`
	Age int	      `json:"age"`
	Skill string
}
func (c caixukun) Intro() {      // reflect.Value.Method()中 i=0
	fmt.Println("大家好,我是练习时长两年半的练习生cxk!")
}
func (c caixukun) Dance() {      // reflect.Value.Method()中 i=1
	fmt.Println("鸡你太美~~丁丁丁丁丁丁丁~鸡你太美~~~丁丁丁丁丁丁丁丁~~")
}
func (c caixukun) Set(n1 int,n2 int) int {      // // reflect.Value.Method()中 i=2
	return n1 + n2
}

func Ref(cxk interface{}) {
	rty := reflect.TypeOf(cxk)    //获取结构体变量的反射类型
	rval := reflect.ValueOf(cxk)  //获取结构体变量的反射值
	rkd := rval.Kind()            //获取结构体变量的类型常量
	//fmt.Printf("%T %v\n",rty,rty)   //*reflect.rtype main.caixukun
	//fmt.Printf("%T %v\n",rval,rval) //reflect.Value {蔡徐坤 20 Ctrl}
	//fmt.Printf("%T %v\n",rkd,rkd)   //reflect.Kind struct
	if rkd != reflect.Struct {          //Struct为常量比较
		fmt.Println("unexpect struct, re-input the correct struct, exit Program~")
		return
	}

	//获取结构体实例所有字段和值
	Fnum := rval.NumField()    //常用的一个方法,获得结构体方法数量,类似len()和 cap() 3个字段
	//接下来演示如何拿到结构体实例的字段和值
	for i := 0 ; i < Fnum ; i++ {
		/*
		Name string   `json:"name"`   //有tag
		Age int	      `json:"age"`    //有tag
		Skill string                  //无tag
		*/
		fmt.Printf("字段索引值为%d,字段值为%v\n",i,rval.Field(i)) //reflect.Value.Field(x)获得字段值
		//检查结构体实例字段的tag标签
		tagRval := rty.Field(i).Tag.Get("json")
		fmt.Printf("第%d个字段为%v,对应的tag为%v\n",i,rval.Field(i),tagRval)
	}

	//获取结构体实例有多少个方法以及方法调用
	methods := rval.NumMethod()
	fmt.Printf("结构体实例存在%d个方法\n",methods)
	
	//reflect.Value.Method(x int)方法中,方法索引排序并不是按照结构体代码由上到下,而是按照方法首字母ASCII代码码值进行
	//所以方法0为:Dance(),方法1为:Intro(),方法2为:Set(xxx),如果方法中本身不传参,Call中必须填入(nil)
	rval.Method(1).Call(nil)   //调用Intro方法: 大家好,我是练习时长两年半的练习生cxk!
	rval.Method(0).Call(nil)   //调用Dance方法: 鸡你太美~~...
	
	//对于要传递参数的方法,传进call()中的是一个[]reflect.Value切片,所以需要先定义切片
	var slice []reflect.Value
	slice = append(slice,reflect.ValueOf(30))   //传第一个参数 进切片
	slice = append(slice,reflect.ValueOf(20))   //传第二个参数 进切片
	fmt.Println(rval.Method(2).Call(slice)[0])  //返回值只有一个值,也是一个切片,所以只需要接收切片第一个值[0]

}

func main() {
	/*
	使用反射来遍历结构体字段,调用结构体的方法,并获得结构体的标签值
	两个重要方法
	1. reflect.Value.Method() :
	func (v Value) Method(i int) Value
	默认按照传入结构体实例的方法名排序对应的i值,初始值i=0
	见上方结构体方法注释,三个方法,分别为方法0,1,2,返回值是一个reflect.Value
	2. reflect.Value.Call() :
	func (v Value) Call(in []Value) []Value
	通过Method获取的几个Value,作为参数形式传入Call()中,调用几种方法,注意[]value为reflect.Value的切片
	 */
	var cxk = caixukun{"蔡徐坤",20,"Ctrl"}
	Ref(cxk)
}
发布了49 篇原创文章 · 获赞 18 · 访问量 3996

猜你喜欢

转载自blog.csdn.net/weixin_41047549/article/details/90579722