(转载)golang 中的反射

我们定义的一个变量,不管是基本类型int,还是一个结构体Student,我们都可以通过reflect.TypeOf()获取他的反射类型Type,也可以通过reflect.ValueOf()去获取他的反射值Value
我们学习反射,其实就是学习如何通过变量,去取得reflect.Type或者reflect.Value;再使用得到的Type以及Value,反过来对变量进行操作
弄明白了这个道理,那一切都将变得简单
剩下的,我们只是需要去学习reflect包中提供的方法。当我们需要要怎么操作变量,就使用其提供的对应方法即可

反射的注意事项与细节

Type与Kind的区别

Type是类型,Kind是类别,听起来有点绕,他们之间的关系为Type是Kind的子集
如果变量是基本类型,那么Type与Kind得到的结果是一致的,比如变量为int类型,Type与Kind的值相等,都为int
但当变量为结构体时,Type与Kind的值就不一样了
我们来看个实际案例

func main() {
    var emp Employee
    emp = Employee{
        Name: "naonao",
        Age:  99,
    }
    rVal := reflect.ValueOf(emp)
    log.Printf("Kind is %v ,Type is %v",
        rVal.Kind(),
        rVal.Type())
    // Kind is struct ,Type is main.Employee
}

可以看到,Kind的值是struct,而Type的值是包名.Employee

在变量与reflect.Value之间切换

变量可以转换成interface{}之后,再转换成reflect.Value类型,既然空接口可以转换成Value类型,那么自然也可以反过来转换成变量
用个表达式来表示,就如下所示
变量<----->interface{}<----->reflect.Value
利用空接口来进行中转,这样变量与Value之间就可以实现互相转换了
下面我们再说如何用代码实现转换

通过反射获取变量本身的值

这里我们要注意一下,reflect.ValueOf()得到的值是reflect.Value类型,并不是变量本身的值

var num = 1
rVal := reflect.ValueOf(num)
log.Printf("num is %v", num + rVal)

这段代码会报错invalid operation: num + rVal (mismatched types int and reflect.Value)
很明显,rVal是属于reflect.Value类型,不能与int类型相加
那怎样才能获得它本身的值呢?
如果是基本类型,比如var num int,那么使用reflect包里提供的转换方法即可reflect.ValueOf(num).Int()
或者是float,那就调用reflect.ValueOf(num).float(),如果是其它的基本类型,需要的时候去文档里面找找即可
但如果是我们自己定义的结构体,因为reflect包无法确定我们自己定义了什么结构体,所以本身并不会带有结构体转换的方法,那么我们只能通过类型断言来进行转换
也就是上面说的,利用空接口进行中转,再利用断言进行类型转换

使用反射来遍历结构体的字段值,并获取结构体的tag标签
// Employee 员工
type Employee struct {
    Name string `json:"emp_name"`
    Age  int    `json:"emp_age"`
    Sex  int
}

// GetSum 返回两数之和
func (e *Employee) GetSum(n1, n2 int) int {
    return n1 + n2
}

// Set 接受值,给结构体e赋值
func (e *Employee) Set(name string, age, sex int) {
    e.Name = name
    e.Age = age
    e.Sex = sex
}

// Print 打印结构体*Employee 
func (e *Employee) Print() {
    log.Print("======Start======")
    log.Print(e)
    log.Print("======End======")
}

//使用反射来遍历结构体的字段值,并获取结构体的tag标签
//先来看个常规用法
// GetStruct 获取结构体的字段及tag
func GetStruct(i interface{}) {
    rType := reflect.TypeOf(i)
    rVal := reflect.ValueOf(i)

    kd := rVal.Kind()

    // 如果是传进来的是指针类型
    // 则获取指针值
    if kd == reflect.Ptr {
        rType = rType.Elem()
        rVal = rVal.Elem()
        kd = rVal.Kind()
    }

    if kd != reflect.Struct {
        log.Panicf("Kind is %v not struct ", kd)
    }
    // 获取结构体的字段数
    sNum := rVal.NumField()
    log.Printf("Struct has %v fields ", sNum)
    // 遍历结构体的所有字段
    for i := 0; i < sNum; i++ {
        log.Printf("Field %d value is %v", i, rVal.Field(i))
        // 获取Struct的tag,使用Type类型获取
        tag := rType.Field(i).Tag.Get("json")
        if tag == "" {
            log.Printf("Field %d hasn't tag  %v ", i, tag)
            continue
        }
        log.Printf("Field %d tag is %v ", i, tag)
    }
}
//复制代码我们定义一个方法GetStruct(i interface{}),因为入参是interface{}类型,所以这个方法可以接收
//并处理所有的数据类型。这就是反射的牛逼之处了
//遗憾的是,反射的性能比较低。后面咱们对性能进行分析时再拿出来聊聊
//测试用例如下
func TestGetStruct(t *testing.T) {
    emp := &Employee{}
    emp.Set("闹闹", 99, 0)
    GetStruct(emp)
}
//这个函数接受的参数是interface,也就是说,通过这个函数,不管入参传递了什么样的结构体,我们可以知道这个结构体有什么标签,
//有几个方法获取tag标签的用处就是对我们的结构体进行序列化时使用,将结构体的字段名变成我们需要的别名
//想深入了解的童鞋,可以参考下encoding/json包的使用方式

获取并调用结构体的方法
// CallMethod 调用结构体方法
// i : 传入的struct
// methodByName : 调用结构体的方法名
func CallMethod(i interface{}, methodByName string) {
    rVal := reflect.ValueOf(i)
    rType := reflect.TypeOf(i)
    log.Printf("Type is %v Kind is %v", rType, rType.Kind())

    // 获取结构体有多少个方法
    numOfMethod := rVal.NumMethod()
    log.Printf("Struct has %d method", numOfMethod)
    // 声明Value数组
    var params []reflect.Value
    // 声明一个Value类型,用于接收方法
    var method reflect.Value

    if methodByName == "GetSum" {
        // 调用方法时的参数
        params = append(params, reflect.ValueOf(10))
        params = append(params, reflect.ValueOf(88))

    }
    if methodByName == "Set" {
        // 调用方法时的参数
        params = append(params, reflect.ValueOf("闹闹吃鱼"))
        params = append(params, reflect.ValueOf(18))
        params = append(params, reflect.ValueOf(0))
    }
    // 获取方法
    method = rVal.MethodByName(methodByName)
    if !method.IsValid() {
        // 如果结构体不存在此方法,输出Panic
        log.Panic("Method is invalid")
    }
    result := method.Call(params)
    if len(result) > 0 {
        // 如果函数存在返回值,则打印第一条
        log.Println("Call result is ", result[0])
    }
}
//这里值得注意一点的就是,我们通过反射的Call去调用函数,传入的参数的类型是reflect.Value类型,并不是我们定义函数时的int类型
//所以在调用函数时传入的参数需要进行一个类型转换
//给你们附上测试用例,你们可以自己调试跑跑,会发现,不管你传的结构体的字段是什么,我都进行统一处理了
func TestCallMethod(t *testing.T) {
    emp := &Employee{}
    emp.Set("闹闹", 99, 0)
    emp.Print()
    CallMethod(emp, "Set")
    emp.Print()
}

修改字段值
// ModifyField 修改字段值
func ModifyField(i interface{}, filedName string) {
    rVal := reflect.ValueOf(i)
    filed := rVal.Elem().FieldByName(filedName)
    if !filed.IsValid() {
        log.Panic("filedName is invalid")
    }
    filed.SetString("闹闹")
}
复制代码运行时修改结构体的字段,主要就是做到一个通用性,比如上述的例子,不管是什么结构体
依然附上测试用例
func TestModifyField(t *testing.T) {
    emp := &Employee{}
    ModifyField(emp, "Name")
}
复制代码不管传入的结构体是什么,只要包含了filedName(我们指定的字段名),我们就可以对其进行值的更改
假如我们有100个结构体,需要对name字段进行修改,通过反射的机制,我们代码的耦合度将大大的降低
定义适配器,用作统一处理接口
// Bridge 适配器
// 可以实现调用任意函数
func Bridge(call interface{}, args ...interface{}) {
    var (
        function reflect.Value
        inValue  []reflect.Value
    )
    n := len(args)
    // 将参数转换为Value类型
    inValue = make([]reflect.Value, n)
    for i := 0; i < n; i++ {
        inValue[i] = reflect.ValueOf(args[i])
    }
    // 获得函数的Value类型
    function = reflect.ValueOf(call)
    // 传参,调用函数
    function.Call(inValue)
}
//写了个测试用例,函数是我们在调用Bridge前就已经定义好了
func TestBridge(t *testing.T) {
    call1 := func(v1, v2 int) {
        log.Println(v1, v2)
    }
    call2 := func(v1, v2 int, str string) {
        log.Println(v1, v2, str)
    }

    Bridge(call1, 1, 2)
    Bridge(call2, 2, 3, "callTest")
}

//两个函数是不同的函数,但是都可以通过Bridge进行执行
//适配器有什么用呢?如果不知道的童鞋,可以去看看设计模式「适配器模式」
//因为本篇幅只是说如何在实战中应用反射,所以这里就不讲解设计模式了
//使用反射创建,并操作结构体
// CreateStruct 使用反射创建结构体
// 并给结构体赋值
func CreateStruct(i interface{}) *Employee {
    var (
        structType  reflect.Type
        structValue reflect.Value
    )
    // 获取传入结构体指向的Type类型
    structType = reflect.TypeOf(i).Elem()
    // 创建一个结构体
    // structValue持有一个指向类型为Type的新申请的指针
    structValue = reflect.New(structType)
    // 转换成我们要创建的结构体
    modle := structValue.Interface().(*Employee)
    // 取得structValue指向的值
    structValue = structValue.Elem()
    // 给结构体赋值
    structValue.FieldByName("Name").SetString("闹闹吃鱼")
    structValue.FieldByName("Age").SetInt(100)
    return modle
}
//使用方式就看看测试用例
func TestCreateStruct(t *testing.T) {
    emp := &Employee{
        Name: "NaoNao",
        Age:  18,
        Sex:  1,
    }
    emp.Print()// &{NaoNao 18 1}
    newEmp := CreateStruct(emp)
    newEmp.Print()// &{闹闹吃鱼 100 0}
}
//可能你会问,CreateStruct的入参不是interface{}吗?可为什么我传一个任意的结构体,却要给返回一个指定的结构体呢?就不能我传什么结构体进去,就返回什么结构体出来吗?
//理想总是丰满的,现实却是非常骨感,虽然我们反射的方法实现,都是将入参写为interface{},但使用反射并不是意味着我们一定就写了一个万能的程序
//我们不管是把Value转为结构体,还是转为基本类型,我们都需要在编译前确定转换后的类型
//换句话说,只要我们在运行时牵扯到类型的转换,我们都需要各种if来判断是否能转换成我们需要的类型
通过反射来修改变量
先来看看代码如何实现

func main() {
    var num = 1
    modifyValue(&num)// 传递地址
    log.Printf("num is %v", num)// num is 20
}

func modifyValue(i interface{}) {
    rVal := reflect.ValueOf(i)
    rVal.Elem().SetInt(20)
}

细心的你肯定发现了一点异常,函数接收的参数不再是值了,而是接受了一个指针地址
改变值的时候,先调用了Elem()方法,再进行了一个SetInt()的操作
为什么直接传值不行呢?因为reflect包中提供的所有修改变量值的方法,都是对指针进行的操作
那为什么还要先使用Elem()呢?因为Elem()的作用,就是取得指针地址所对应的值,取到值了,我们才能对值进行修改
总不可能连值都没拿到手,就想着去改值吧?

理解reflect.Value.Elem()
关于Elem()的使用可以简单的理解为
num := 1
prt *int := &num // 获取num的指针地址
num2 := *ptr // 从指针处取值

因为我们传递了一个地址,所以我们要先拿到这个地址的指针,再通过指针去取得所对应的值
reflect包底层实现就是基于这个原理,只是它的底层代码加了较多的判断,用来保证稳定性

golang的反射与实践

你可能感兴趣的:((转载)golang 中的反射)