当前位置:首页 > 文章列表 > Golang > Go教程 > Go语言的反射机制详解

Go语言的反射机制详解

来源:脚本之家 2022-12-28 15:13:52 0浏览 收藏

在Golang实战开发的过程中,我们经常会遇到一些这样那样的问题,然后要卡好半天,等问题解决了才发现原来一些细节知识点还是没有掌握好。今天golang学习网就整理分享《Go语言的反射机制详解》,聊聊反射、机制,希望可以帮助到正在努力赚钱的你。

反射是语言里面是非常重要的一个特性,我们经常会看见这个词,但是对于反射没有一个很好的理解,主要是因为对于反射的使用场景不太熟悉。

一、理解变量的内在机制

1.类型信息,元信息,是预先定义好的,静态的。

2.值信息,程序进行过程中,动态变化的。

二、反射和空接口

1.空接口相当于一个容器,能接受任何东西。

2.那怎么判断空接口变量存储的是什么类型呢?之前有使用过类型断言,这只是一个比较基础的方法

3.如果想获取存储变量的类型信息和值信息就要使用反射机制,所以反射是什么? 反射就是动态的获取变量类型信息和值信息的机制。

三、怎么利用反射分析空接口里面的信息呢?

①首先利用的是GO语言里面的Reflect包

②利用包里的TypeOf方法可以获取变量的类型信息

func reflect_typeof(a interface{}) {
    t := reflect.TypeOf(a)
    fmt.Printf("type of a is:%v\n", t)
 
    k := t.Kind()
    switch k {
    case reflect.Int64:
        fmt.Printf("a is int64\n")
    case reflect.String:
        fmt.Printf("a is string\n")
    }
}

利用Kind() 可以获取t的类型,如代码所示,这里可以判断a是Int64还是string, 像下面一样使用:

func main() {
    var x int64 = 3
    reflect_example(x)
 
    var y string = "hello"
    reflect_example(y)
}

打印结果:

type of a is:int64
a is int64
type of a is:string
a is string

③利用包里的ValueOf方法可以获取变量的值信息

func reflect_value(a interface{}) {
    v := reflect.ValueOf(a)
    k := v.Kind()
    switch k {
    case reflect.Int64:
        fmt.Printf("a is Int64, store value is:%d\n", v.Int())
    case reflect.String:
        fmt.Printf("a is String, store value is:%s\n", v.String())
    }
}

利用ValueOf方法可以得到变量的值信息,ValueOf返回的是一个Value结构体类型,有趣的是 可以使用 v.Type() 获取该变量的类型,和上面reflect.TypeOf() 获取的结果一样。

此外,因为值信息是动态的,所以我们不仅仅可以获取这个变量的类型,还能取得这个变量里面存储的值,利用 v.Int() 、 v.String() 等等就能取得值。如上面的main,调用此函数返回的结果:

a is Int64, store value is:3
a is String, store value is:hello

这里存在一个问题,如果传进去一个类型,使用了错误的解析,那么将会在运行的时候报错, 例如将 一个string类型强行的v.Int()。

既然值类型是动态的,能取到保存的值,同样可以设置值。在反射里面有很多set的方法,例如SetFloat、SetInt()、SetString()等可以帮助我们设置值。

下面的例子,我想把 x设置为 6.28,但是会报错!

func main() {
    var x float64 = 3.14
    v := reflect.ValueOf(x)
    v.SetFloat(6.28)
    fmt.Printf("After Set Value is %f", x)
}

错误结果:

panic: reflect: reflect.Value.SetFloat using unaddressable value
......

结果上说明是不可设置的,为什么呢? 因为我们的x是一个值类型,而值类型的传递是拷贝了一个副本,当 v := reflect.ValueOf(x) 函数通过传递一个 x 拷贝创建了 v,那么 v 的改变并不能更改原始的 x。要想 v 的更改能作用到 x,那就必须传递 x 的地址 v = reflect.ValueOf(&x)。修改程序如下:

func main() {
    var x float64 = 3.14
    v := reflect.ValueOf(&x)
    v.SetFloat(6.28)
    fmt.Printf("After Set Value is %f", x)
}

结果:依然报错!为什么传了地址还报错?因为&x是地址了,所以它的类型就变了,可以通过v.Type(),看下改变成了什么:

func main() {
    var x float64 = 3.14
    v := reflect.ValueOf(&x)
    fmt.Printf("type of v is %v", v.Type())   //打印的结果是:type of v is *float64
}

由程序可以知道,这个返回的是一个指针类型的。所以上面SetFloat才会失败,那怎么做?

我们正常的赋值,如果是地址的话,例如下面:一般我们都会对*y进行赋值, *的意思就是往这个地址里面赋值。

var y *float64 = new(float64)
*y = 10.12
fmt.Printf("y = %v", *y)

同样的,我们在反射里面也可以取地址,需要通过 Elem() 方法进行取地址。再次修改程序

func main() {
    var x float64 = 3.14
    v := reflect.ValueOf(&x)
    fmt.Printf("type of v is %v\n", v.Type())
    v.Elem().SetFloat(6.28)
    fmt.Printf("After set x is %v", x)
}

结果为:

type of v is *float64
After set x is 6.28

四、利用反射获取结构体里面的方法和调用。

1.获取结构体的字段

我们可以通过上面的方法判断一个变量是不是结构体。

可以通过 NumField() 获取所有结构体字段的数目、进而遍历,通过Field()方法获取字段的信息。

type Student struct {
    Name  string
    Sex   int
    Age   int
    Score float32
}
 
func main() {
    //创建一个结构体变量
    var s Student = Student{
        Name:  "BigOrange",
        Sex:   1,
        Age:   10,
        Score: 80.1,
    }
 
    v := reflect.ValueOf(s)
    t := v.Type()
    kind := t.Kind()
     
    //分析s变量的类型,如果是结构体类型,那么遍历所有的字段
    switch kind {
    case reflect.Int64:
        fmt.Printf("s is int64\n")
    case reflect.Float32:
        fmt.Printf("s is int64\n")
    case reflect.Struct:
        fmt.Printf("s is struct\n")
        fmt.Printf("field num of s is %d\n", v.NumField())
        //NumFiled()获取字段数,v.Field(i)可以取得下标位置的字段信息,返回的是一个Value类型的值
        for i := 0; i 

执行结果:

s is struct
field num of s is 4
name:Name type:string value:BigOrange
name:Sex type:int value:1
name:Age type:int value:10
name:Score type:float32 value:80.1

这里需要说明几个问题:

①打印字段名称的时候,使用的是t.Field(i).Name ,Name是静态的,所以属于类型的信息

②打印值的时候,这里将field.Interface()实际上相当于ValueOf的反操作(可以参考这篇文章https://www.jb51.net/article/255856.htm),所以才能把值打印出来

③此外如果Student中的Name字段变为name(私有),那么则会报错,不能反射出私有变量 错误信息 “panic: reflect.Value.Interface: cannot return value obtained from unexported field or method”

2.对结构体内的字段进行赋值操作

参考下面的代码,对上面的Student进行赋值操作:

func main() {
    s := Student{
        Name:  "BigOrange",
        Sex:   1,
        Age:   10,
        Score: 80.1,
    }
 
    fmt.Printf("Name:%v, Sex:%v,Age:%v,Score:%v \n", s.Name, s.Sex, s.Age, s.Score)
    v := reflect.ValueOf(&s)  //这里传的是地址!!!
 
    v.Elem().Field(0).SetString("ChangeName")
    v.Elem().FieldByName("Score").SetFloat(99.9)
 
    fmt.Printf("Name:%v, Sex:%v,Age:%v,Score:%v \n", s.Name, s.Sex, s.Age, s.Score)
}

结果:

Name:BigOrange, Sex:1,Age:10,Score:80.1
Name:ChangeName, Sex:1,Age:10,Score:99.9

3.获取结构体里面的方法

可以通过NumMethod()获得接头体里面的方法数量,然后遍历通过Method()获取方法的具体信息。如下代码所示:

//新增-设置名称方法
func (s *Student) SetName(name string) {
     fmt.Printf("有参数方法 通过反射进行调用:%v\n", s)
     s.Name = name
}
//新增-打印信息方法
func (s *Student) PrintStudent() {
    fmt.Printf("无参数方法 通过反射进行调用:%v\n", s)
}
 
func main() {
    s := Student{
        Name:  "BigOrange",
        Sex:   1,
        Age:   10,
        Score: 80.1,
    }
 
    v := reflect.ValueOf(&s)
    //取得Type信息
    t := v.Type()
     
    fmt.Printf("struct student have %d methods\n", t.NumMethod())
 
    for i := 0; i 

输出:

struct student have 2 methods
struct 0 method, name:PrintStudent type:func(*main.Student)
struct 1 method, name:SetName type:func(*main.Student, string)

从结果中看到我们可以获取方法的名称以及签名信息,并且这个方法的输出顺序是按照字母排列的。

并且输出结果可以看到一个有趣的现象:结构体的方法其实也是通过函数实现的例如 func(s Student) SetName(name string) 这个方法,反射之后的结果就是 func(main.Student , string) 实际上把Student当参数了。

此外还可以通过反射来调用这些方法。想要通过反射调用结构体里面的方法,首先要知道方法调用时一个动态的,所以要先通过ValueOf获取值,然后通过获取的值进行方法的调用 ,通过 value里面的Method方法 返回一个方法,然后通过Call方法调用,Call是参数是一个切片,也就是参数的列表。以下是Call方法的定义:可以看到参数是一个Value的数组:

如下代码展示了如何调用有参数的方法和无参数的方法:

func main() {
    s := Student{
        Name:  "BigOrange",
        Sex:   1,
        Age:   10,
        Score: 80.1,
    }
 
    v := reflect.ValueOf(&s)
 
    //通过reflect.Value获取对应的方法并调用
    m1 := v.MethodByName("PrintStudent")
    var args []reflect.Value
    m1.Call(args)
 
    m2 := v.MethodByName("SetName")
    var args2 []reflect.Value
    name := "stu01"
    nameVal := reflect.ValueOf(name)
    args2 = append(args2, nameVal)
    m2.Call(args2)
    m1.Call(args)
}

执行结果:

无参数方法 通过反射进行调用:&main.Student{Name:"BigOrange", Sex:1, Age:10, Score:80.1}
有参数方法 通过反射进行调用:&main.Student{Name:"BigOrange", Sex:1, Age:10, Score:80.1}
无参数方法 通过反射进行调用:&main.Student{Name:"stu01", Sex:1, Age:10, Score:80.1}

上面格式打印:

  • %v 相应值的默认格式。 Printf("%v", people) {zhangsan},
  • %+v 打印结构体时,会添加字段名 Printf("%+v", people) {Name:zhangsan}
  • %#v 相应值的Go语法表示 Printf("#v", people) main.Human{Name:"zhangsan"}

五、怎么获取结构体里tag的信息。

有时候我们在类型上面定义一些tag,例如使用json和数据库的时候。Field()方法返回的StructField结构体中保存着Tag信息,并且Tag信息可以通过一个Get(Key)的方法获取出来,如下代码所示:

type Student struct {
    Name string `json:"jsName" db:"dbName"`
}
 
func main() {
    s := Student{
        Name: "BigOrange",
    }
    v := reflect.ValueOf(&s)
    t := v.Type()
    field0 := t.Elem().Field(0)
    fmt.Printf("tag json=%s\n", field0.Tag.Get("json"))
    fmt.Printf("tag db=%s\n", field0.Tag.Get("db"))
}

结果:

tag json=jsName
tag db=dbName

六、应用场景

1.序列化和反序列化,比如json, protobuf等各种数据协议

2.各种数据库的ORM,比如gorm,sqlx等数据库中间件

3.配置文件解析相关的库,比如yaml、ini等

好了,本文到此结束,带大家了解了《Go语言的反射机制详解》,希望本文对你有所帮助!关注golang学习网公众号,给大家分享更多Golang知识!

版本声明
本文转载于:脚本之家 如有侵犯,请联系study_golang@163.com删除
Go语言中的IO操作及Flag包的用法Go语言中的IO操作及Flag包的用法
上一篇
Go语言中的IO操作及Flag包的用法
Golang中Interface接口的三个特性
下一篇
Golang中Interface接口的三个特性
评论列表
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    542次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    508次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    497次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    484次学习
查看更多
AI推荐
  • 笔灵AI生成答辩PPT:高效制作学术与职场PPT的利器
    笔灵AI生成答辩PPT
    探索笔灵AI生成答辩PPT的强大功能,快速制作高质量答辩PPT。精准内容提取、多样模板匹配、数据可视化、配套自述稿生成,让您的学术和职场展示更加专业与高效。
    14次使用
  • 知网AIGC检测服务系统:精准识别学术文本中的AI生成内容
    知网AIGC检测服务系统
    知网AIGC检测服务系统,专注于检测学术文本中的疑似AI生成内容。依托知网海量高质量文献资源,结合先进的“知识增强AIGC检测技术”,系统能够从语言模式和语义逻辑两方面精准识别AI生成内容,适用于学术研究、教育和企业领域,确保文本的真实性和原创性。
    22次使用
  • AIGC检测服务:AIbiye助力确保论文原创性
    AIGC检测-Aibiye
    AIbiye官网推出的AIGC检测服务,专注于检测ChatGPT、Gemini、Claude等AIGC工具生成的文本,帮助用户确保论文的原创性和学术规范。支持txt和doc(x)格式,检测范围为论文正文,提供高准确性和便捷的用户体验。
    30次使用
  • 易笔AI论文平台:快速生成高质量学术论文的利器
    易笔AI论文
    易笔AI论文平台提供自动写作、格式校对、查重检测等功能,支持多种学术领域的论文生成。价格优惠,界面友好,操作简便,适用于学术研究者、学生及论文辅导机构。
    39次使用
  • 笔启AI论文写作平台:多类型论文生成与多语言支持
    笔启AI论文写作平台
    笔启AI论文写作平台提供多类型论文生成服务,支持多语言写作,满足学术研究者、学生和职场人士的需求。平台采用AI 4.0版本,确保论文质量和原创性,并提供查重保障和隐私保护。
    35次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码