go语言基础 -- 反射

反射的基本介绍

  1. 反射可以在运行时动态获取变量的信息,如变量的类型(type),类别(kind)。
  2. 如果是结构体变量,还可以获取到变量的字段、方法等结构体本身信息;
  3. 通过反射,可以修改变量的值或调用关联的方法;
  4. 使用反射需要import("reflect")
    我们前面的文章空接口接收任意类型的变量,通过typeof来判断变量类型

反射的基础应用场景

  1. 不知道接口调用哪个函数,根据传入参数在运行时确定调用的具体接口,这时需要对函数或方法进行反射,如下例传入函数指针,及回调函数的参数
    func bradge(funcPtr interface{}, args ...interface{})
    在bradge中通过反射来执行传入的函数
  2. 结构体序列化时,指定了字段tag,通过反射生成对应的字符串

反射的重要函数

  1. reflect.TypeOf(变量名)--获取变量的类型,返回值是reflect.Type类型(是一个接口类型)
  2. reflect.ValueOf(变量名)--获取变量的值,返回reflect.Value类型(是一个struct),具体可查go的反射包的介绍
变量、interface{}、reflect.Value是可以相互转换的

我们来看一个相互转换的例子,这在反射中是很常用的

go 复制代码
var student Student
var num nt

func trans(b interface{}) {
    // 将b这个interface{}类型转为reflect.Value类型
    rVal := reflect.ValueOf(b)
    // 将reflect.Value类型转为interface{}类型
    iVal := rVal.Interface()
    // 将interface类型转为原来的变量类型--类型断言
    val := iVal.(Student)

    // 若b是int类型,还可以直接用reflect.Value类型里面的Int方法取变量值,之后才能和其他int类型进行操作
    n := rVal.Int()
    
}
  1. reflect.Value.kind和reflect.Value.type的区别
    kind是变量的分类,是大的范畴

    kind的取值是枚举里面的值,是一个常量,type是变量的具体类型,type的取值可以是自定义类型,比如一个自定义结构体类型Student,它的kind是struct,但type是Student
  2. 在使用反射的方式来获取变量的值并获取对应的数据类型,必须类型一致,比如rVal := reflect.ValueOf(b),假设这里的rVal的值是一个int类型,但我们用rVal.Float()来获取值,那么就会panic。
  3. 通过反射来修改变量时,使用SetXxx的方法来修改变量值,这时需要传递变量的指针,同时需要用到reflect.Elem().SetXxx()而不是reflect.SetXxx()
go 复制代码
func main() {
    var num int = 10
    // 这里需要传地址
    rVal = reflect.ValueOf(&num)
    // 这里不能直接set,需要用指针指向的elem来修改
    rVal.Elem().SetInt(20)
}

反射的几种最佳实践

1. 使用反射遍历结构体字段,调用结构体方法,并获取结构体标签

go 复制代码
package main
import(
    "fmt"
    "reflect"
)
type Monster struct {
    Name string `json:name`
    Age int `json:monster_age`
    Score float32
    Sex string
}

func (s Monster)Print() {
    fmt.Println("print monster", s)
}

func (s Monster)GetSum(n1, n2 int) int {
    return n1 + n2
}

func (s Monster) Set(Name string, Age int, Score float32, Sex string) {
    s.Name = Name
    s.Age = Age
    s.Score = Score
    s.Sex = Sex
}

func TestStruct(b interface{}) {
    rtype := reflect.TypeOf(b)
    rVal := reflect.ValueOf(b)
    kd := rVal.Kind()
    if kd != reflect.Struct {
        fmt.Println("expect struct")
        return
    }

    // 获取该结构体有几个字段
    num := rVal.NumField()
    fmt.Printf("struct has %d filed", num)
    for i :=0; i < num; i++ {
        // 通过Field方法遍历字段,注意这里Field(i)是reflect.Value类型,不能直接用于运算,也需要进行类型断言才能运算
        fmt.Printf("field %d value is %v", i, rVal.Field(i))
        // 这里不能用value获取tag,只能用reflect.Type来获取标签,如果结构体不止一个标签,那么这里只能获取json的标签数据
        tagval := rtype.Field(i).Tag.Get("json")
        if tagval != "" {
            fmt.Printf("field %d tag is:%v", i, tagval)
        }
    }

    //获取到该结构体有多少个方法,方法序号从0开始,方法的排序默认按照函数名的ascii码大小比较排序
    numOfMethod := val.NumMethod()
    fmt.Printf("struct has %d methods\n", numOfMethod)
    // var params []reflect.Value
    // 调用第二个方法:Print方法
    rval.Method(1).Call(nil)
    // 调用结构体的第1个方法Method(0)
    var params []reflect.Value
    params append(params, reflect.Valueof(10))
    params append(params, reflect.Valueof(40))
    // Call方法参数类型reflect.Value类型的切片类型,因此上面先构造切片,返回值也是一个切片
    res := rval.Method(0).Call(params)//传入的参数是[]reflect.Value
    fmt.Println("res=",res[0].Int())//返回结果,返回的结果是[]reflect.Value*/
    
}

func main() {
    var m1 Monster := Monster{
        Name : "huangshulang",
        Age : 400,
        Score : 30.9,
    }
    // 将Monster实例传给TestStruct函数即可调用结构体相关的方法,这在一些固定框架的代码中很有用,我们通常只需要定义好结构体及对应的方法,框架里面通过反射即可相关功能自动处理,无需显式通过结构体实例调用
    TestStruct(m1)
}

补充上面Call方法的官方说明:

2、修改结构体、标签

这个基本上与上面一致,需要注意的市,我们在传参的时候,TestStruct(&m1)要传地址,才能修改成功,其余的就是用类似

rval.Elem().Field(0).SetString("白象精")修改方法来操作修改即可

3、适配器实现

定义了两个函数test1和test2,定义一个适配器函数用作统一处理接口

(1)定义了两个函数

test1 := func(v1 int, v2 int){

t.Log(v1,v2)

}

test2 := func(v1 int, v2 int, s string){

t.Log(v1,v2,s)

}

现在要定义一个适配器函数用作统一处理接口,其大致结构如下

bridge:=func(call interface{}, args.....interface{}){

// 内容

}

实现调用test1对应的函数

bridge(test1, 1, 2)

实现调用test2对应的函数

bridge(test2, 1, 2, "test2")

go 复制代码
func TestReflectFunc(t *testing.T){
    call1 := func(v1 int,v2 int){
        t.Log(v1,v2)
    }
    call2 := func(v1 int,v2 int,s string){
        t.Log(v1,v2,s)
    }
    var(
        function reflect.Value
        inValue []reflect.Value
        n int
    )
    bridge := func(call interface{}, args...interface{}){
    n = len(args)
    inValue=make(reflect.Value, n)
    for i := 0; i<n; i++ {
       inValue[i]=reflect.ValueOf(args[i])
    }
    function reflect.ValueOf(call)
    function.Call(inValue)
}
bridge(call1,1,2)
bridge(call2,1,2,"test2")

4、使用反射操作任意结构体

go 复制代码
type user struct {
    Userld string
    Name string
}
func TestReflectStruct(t *testing.T) {
    var ( 
        model *user
        sv reflect.Value
    )
    // model指向一个user对象
    model = &user{}
    // 将model转成一个reflect.Value类型
    sv = reflect.ValueOf(model)
    t.Log("reflect.ValueOf",sv.Kind().String())
    // 这里获取Elem之后,sv实际就指向user对象了,后续就能直接用sv来操作结构体了
    sv = sv.Elem()
    t.Log("reflect.ValueOf.Elem",sv.Kind().String())
    sv.FieldByName("Userld").SetString("12345678")
    sv.FieldByName("Name").SetString("nickname")
    t.Log("model",model)
}

5、使用反射创建并操作结构体

go 复制代码
package test
import (
    "testing"
    "reflect"
)
type user struct {
    Userld string
    Name string
}
func TestReflectStructPtr(t *testing.T) {
    var(
        model *user
        st reflect.Type
        elem reflect.Value
    )
    st = reflect.Typeof(model)/获取类型*user
    t.Log("reflect.Typeof",st.Kind().String())//ptr
    st = st.Elem()/st指向的类型
    t.Log("reflect.TypeOf.Elem",st.Kind).String())//struct
    elem = reflect.New(st)/New返回一个Value类型值,该值持有一个指向类型为type的新申请的零值的指针
    t.Log("reflect.New",elem.Kind().String())//ptr
    t.Log("reflect.New.Elem",elem.Elem().Kind().StringO)//struct
    /model就是创建的user结构体变量(实例)
    model = elem.Interface().(*user)/model:是user它的指向和elem是一样的
    elem = elem.Elem()/取得elem指向的值
    elem.FieldByName("Userld").SetString("12345678")/赋值.
    elem.FieldByName("Name").SetString("nickname")
    t.Log("modelmodel.Name", model ,model.Name)
相关推荐
流星白龙12 分钟前
【C++习题】10.反转字符串中的单词 lll
开发语言·c++
尘浮生19 分钟前
Java项目实战II基于微信小程序的校运会管理系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
MessiGo19 分钟前
Python 爬虫 (1)基础 | 基础操作
开发语言·python
Tech Synapse25 分钟前
Java根据前端返回的字段名进行查询数据的方法
java·开发语言·后端
.生产的驴26 分钟前
SpringCloud OpenFeign用户转发在请求头中添加用户信息 微服务内部调用
spring boot·后端·spring·spring cloud·微服务·架构
乌啼霜满天24933 分钟前
JDBC编程---Java
java·开发语言·sql
微信-since8119241 分钟前
[ruby on rails] 安装docker
后端·docker·ruby on rails
色空大师1 小时前
23种设计模式
java·开发语言·设计模式
Bruce小鬼1 小时前
QT文件基本操作
开发语言·qt
2202_754421541 小时前
生成MPSOC以及ZYNQ的启动文件BOOT.BIN的小软件
java·linux·开发语言