深入理解go语言反射机制

2024-06-24 02:04

本文主要是介绍深入理解go语言反射机制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1、前言

       每当我们学习一个新的知识点时,一般来说,最关心两件事,一是该知识点的用法,另外就是使用场景。go反射机制作为go语言特性中一个比较高级的功能,我们也需要从上面两个方面去进行学习,前者告诉我们如何去使用,而后者告诉我们为什么以及什么时候使用。

2、反射机制

2.1 反射机制的基本概念

        在 Go 语言中,反射机制允许程序在运行时获取对象的类型信息、访问对象的字段和方法动态调用方法,甚至修改变量的值。反射的核心是 reflect 包,它提供了一组函数和类型来操作任意类型的值。

反射的基本概念

  1. 类型和值:反射通过 reflect.Typereflect.Value 来表示变量的类型和值。
  2. 类型检查:可以使用反射来检查变量的类型,包括基础类型和复杂类型。
  3. 值操作:可以通过反射读取和修改变量的值,但需要注意可设置性(settable)。

2.2 反射的基本用法

2.2.1 获取变量的类型和值

        可以通过reflect.TypeOf() 以及reflect.ValueOf()两个方法动态获取变量的类型和值。

package mainimport ("fmt""reflect"
)func main() {var x float64 = 3.4// 通过reflect.TypeOf方法获取变量的类型// 返回类型为reflect.Typet := reflect.TypeOf(x)// 通过reflect.ValueOf方法获取变量的值// 返回类型为reflect.Valuev := reflect.ValueOf(x)fmt.Println("Type:", t)fmt.Println("Value:", v)
}

 程序打印如下:

Type: float64
Value: 3.4

2.2.2 修改反射对象的值

        通过 reflect.Value.CanSet 来判断一个反射对象是否是可设置的。如果是可设置的,我们就可以通过 reflect.Value.Set 来修改反射对象的值。

       那么什么是可设置的值呢?可设置性(settable)——反射中某些值是可修改的,而某些值是不可修改的。只有通过指针传递的值才能被修改。这是因为 Go 语言中的值传递特性——非指针变量的值是不可修改的。

        Go 语言中,变量是通过值传递的。对于普通变量(非指针变量),反射只能读取它们的值,不能修改它们。要使一个变量的值可通过反射修改,必须传递其地址(即指针),因为指针允许间接修改变量的值。

        一句话概括就是,值是不可更改的变量的引用(指针解引用),就可以修改

package mainimport ("fmt""reflect"
)func main() {var x float64 = 3.4// 获取的是变量x的值v := reflect.ValueOf(x)// 获取的是变量x地址的值pv := reflect.ValueOf(&x)// 获取的是变量x的地址的解引用,就是变量x的引用refv := reflect.ValueOf(&x).Elem()fmt.Println("can set:", v.CanSet())fmt.Println("can set:", pv.CanSet())fmt.Println("can set:", refv.CanSet())if refv.CanSet() {refv.SetFloat(2.7)fmt.Println("value after change:", x)}}

输出结果如下:

can set: false
can set: false
can set: true
value after change: 2.7

 2.2.3 动态获取和修改结构体中的字段和值

        在2.2.1中提到可以通过reflect.TypeOf()以及reflect.ValueOf()两个方法来获取一个变量的类型和值,这两个方法的返回类型分别为reflect.Typereflect.Value。这两个类型还提供了一些非常有用的方法来处理结构体变量。

对于reflect.Type, 下面列出部方法:

type Type interface {// 返回此类型的特定种类Kind() Kind// 返回结构类型的字段数量NumField() int// 返回结构体名称Name() string// 返回结构体种的第 i 个字段Field(i int) StructField// 返回具有给定名称的结构字段,并返回一个布尔值,指示是否找到该字段。FieldByName(name string) (StructField, bool)// 返回结构体中的第 i 个方法Method(i int) Method// 返回结构体中指定的方法,并返回是否找到该方法的bool值MethodByName(string) (Method, bool)// 返回可访问的方法数量NumMethod() int}

Kind方法额外作一点说明 

Kind()方法返回的是变量的类型所属的种类(额……听起来有点绕),举个栗子就明白了:

 

package mainimport ("fmt""reflect"
)type Person struct {Name stringAge  int
}func main() {i := 100s := "hello"p := Person{"Alice", 30}fmt.Printf("type:%s, kind:%s\n", reflect.TypeOf(i), reflect.TypeOf(i).Kind())fmt.Printf("type:%s, kind:%s\n", reflect.TypeOf(s), reflect.TypeOf(s).Kind())fmt.Printf("type:%s, kind:%s\n", reflect.TypeOf(p), reflect.TypeOf(p).Kind())
}

打印结果如下:

type:int, kind:int
type:string, kind:string
type:main.Person, kind:struct 

 简单总结就是reflect.TypeOf()方法得到的是变量的类型(int, *int, string, Person等),reflect.TypeOf().Kind()方法得到是变量类型所属的类别(int,ptr,string,struct)。

 reflect.Value 方法如下:

  • 获取信息类方法

Type():获取值的类型(返回 reflect.Type

Kind():获取值的种类(返回 reflect.Kind,如 reflect.Intreflect.Struct 等)。

Interface():将 reflect.Value 转换为 interface{},可以恢复为原始类型。

IsValid():检查值是否有效。

CanSet():检查值是否可设置。

// Type方法获取值类型
v := reflect.ValueOf(42)
fmt.Println(v.Type()) // 输出: int
v := reflect.ValueOf(42)
// Kind方法获取类型的所属的类
fmt.Println(v.Kind()) // 输出: int
v := reflect.ValueOf(42)
// Interface方法转换为接口
i := v.Interface().(int)
fmt.Println(i) // 输出: 42
// IsValid判断值是否有效
var v reflect.Value
fmt.Println(v.IsValid()) // 输出: false
// CanSet判断值是否可以设置
v := reflect.ValueOf(42)
fmt.Println(v.CanSet()) // 输出: falsevp := reflect.ValueOf(&v).Elem()
fmt.Println(vp.CanSet()) // 输出: true

  • 获取/修改基础类型的值方法

无论值多么复杂的结构,最终的字段都可以拆解为基础类型。如果refect.Type为基础类型,那么就可以获取值。

Int():获取整数值(适用于 intint8int16int32int64)。SetInt():设置整数值。

Float():获取浮点数值(适用于 float32float64)。SetFloat():设置浮点数值。

String():获取字符串值。SetString():设置字符串值。

Bool():获取布尔值。SetBool():设置布尔值。

// 获取基础类型的值
v := reflect.ValueOf(42)
fmt.Println(v.Int()) // 输出: 42
// 设置基础类型的值
var x int64 = 42
v := reflect.ValueOf(&x).Elem()
v.SetInt(43)
fmt.Println(x) // 输出: 43
  • 处理复合结构体

FieldByName():获取结构体字段的值。

Field(i):获取结构体第i个字段的值。

Elem():获取指针指向的(解引用)值。

type Person struct {Name stringAge  int
}
p := Person{"Alice", 30}
v := reflect.ValueOf(p)
// 根据字段名获取字段值
nameField := v.FieldByName("Name")
fmt.Println(nameField.String()) // 输出: Alice
// 获取第i个字段的值
nameFieldI := v.Field(0)
fmt.Println(nameField.String()) // 输出: Alice
// 获取指针变量解引用的值
x := 42
v := reflect.ValueOf(&x)
e := v.Elem()
fmt.Println(e.Int()) // 输出: 42

2.2.4 获取变量的类型和值的用法总结 

  • 可以通过reflect.TypeOf()和reflect.ValueOf()分别获取变量的类型(reflect.Type)和值(reflect.Value)
  • 通过Kind方法可以获取一个值的类型所属的类别(int等基础类型、struct、ptr等)
  • 如果Kind方法返回的是基础类型,那么可以直接通过Int(),String()等方法获取变量的值
  • 如果Kind方法返回的是ptr类型,那么可以通过Elem()对指针解引用得到指针变量的值
  • 如果Kind方法返回的是struct类型,那么需要通过Field(i)或FieldByName()逐个获取每个字段的值,然后再根据每个字段的值的类型做进一步操作。

3、反射机制的使用场景

3.1 反射机制有什么用

        存在即是需要,反射机制这种语言特被开发出来,肯定是编程需要。

Go语言的反射(reflection)是指在程序运行时检查类型信息和变量值的能力。通过反射,我们可以在运行时动态地获取和修改对象的属性、方法和类型信息。

反射的作用主要有以下几个方面:

  1. 动态类型识别:反射可以在运行时动态地识别一个接口变量所存储的具体类型,包括基本类型、结构体类型、函数类型等。这样就可以根据具体类型来执行不同的操作。

  2. 动态创建对象:反射可以动态地创建一个对象的实例,包括结构体、数组、切片、Map等。这在编写通用代码时非常有用,可以根据输入参数的类型动态创建相应类型的对象。

  3. 动态调用方法和函数:反射可以在运行时动态地调用一个对象的方法或函数,包括公开的和私有的方法。这样就可以在不知道具体类型的情况下调用相应的方法或函数。

  4. 动态修改对象的属性:反射可以在运行时动态地修改对象的属性值,包括公开的和私有的属性。这在需要动态修改对象状态的情况下非常有用。

  5. 对结构体的字段进行遍历和操作:反射可以遍历一个结构体的所有字段,并对字段进行读取、修改等操作。这在需要根据结构体字段进行一些通用操作的场景下非常有用。

3.2 反射机制使用场景

Go 语言的反射机制允许在运行时检查和操作类型和值。反射通常用于以下几种场景:

  1. 通用库和框架开发:例如,ORM(对象关系映射)框架需要根据结构体定义来生成数据库查询。
  2. 序列化和反序列化:如 JSON 或 XML 的编解码,处理结构体字段与数据格式之间的转换。
  3. 单元测试:在测试中检查结构体或接口的类型和值。
  4. 动态调用方法和访问字段:在不知道具体类型的情况下,通过接口访问和修改对象。

以场景1为例:  

下面这个例子借用了用手写一个工具的过程讲清楚Go反射的使用方法和应用场景 文章中代码。

假如要根据一个结构体来生成查询数据库的SQL语句,该怎么做呢?

如果结构体是一个已知的类型,那么十分简单,关键代码只需要一行就能搞定:

package mainimport (  "fmt"
)type order struct {  ordId      intcustomerId int
}func createQuery(o order) string {  i := fmt.Sprintf("INSERT INTO order VALUES(%d, %d)", o.ordId, o.customerId)return i
}func main() {  o := order{ordId:      1234,customerId: 567,}fmt.Println(createQuery(o))
}

打印如下:

INSERT INTO order VALUES(1234, 567)

 分析下面这行关键代码:

fmt.Sprintf("INSERT INTO order VALUES(%d, %d)", o.ordId, o.customerId)

在生成Sql语句时,%d来格式化,说明我们知道插入的值的类型为int,另外,我们能够使用o.ordId以及o.customerId,说明我们知道结构中的字段是ordId以及customerId。

如果要写一个通用的方法,这样显然是不行的,因为不知道结构体中有哪些字段,更不知道这些字段的值的类型,这些都是需要动态获取的。

废话不多说,直接上代码:

package mainimport ("fmt""reflect"
)type order struct {ordId      intcustomerId int
}type employee struct {name    stringid      intaddress stringsalary  intcountry string
}func createQuery(q interface{}) string {t := reflect.TypeOf(q)v := reflect.ValueOf(q)if v.Kind() != reflect.Struct {panic("unsupported argument type!")}tableName := t.Name() // 通过结构体类型提取出SQL的表名sql := fmt.Sprintf("INSERT INTO %s ", tableName)columns := "("values := "VALUES ("for i := 0; i < v.NumField(); i++ {// 注意reflect.Value 也实现了NumField,Kind这些方法// 这里的v.Field(i).Kind()等价于t.Field(i).Type.Kind()switch v.Field(i).Kind() {case reflect.Int:if i == 0 {columns += fmt.Sprintf("%s", t.Field(i).Name)values += fmt.Sprintf("%d", v.Field(i).Int())} else {columns += fmt.Sprintf(", %s", t.Field(i).Name)values += fmt.Sprintf(", %d", v.Field(i).Int())}case reflect.String:if i == 0 {columns += fmt.Sprintf("%s", t.Field(i).Name)values += fmt.Sprintf("'%s'", v.Field(i).String())} else {columns += fmt.Sprintf(", %s", t.Field(i).Name)values += fmt.Sprintf(", '%s'", v.Field(i).String())}}}columns += "); "values += "); "sql += columns + valuesfmt.Println(sql)return sql
}func main() {o := order{ordId:      456,customerId: 56,}createQuery(o)e := employee{name:    "Naveen",id:      565,address: "Coimbatore",salary:  90000,country: "India",}createQuery(e)
}

这篇关于深入理解go语言反射机制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1088878

相关文章

C语言中联合体union的使用

本文编辑整理自: http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=179471 一、前言 “联合体”(union)与“结构体”(struct)有一些相似之处。但两者有本质上的不同。在结构体中,各成员有各自的内存空间, 一个结构变量的总长度是各成员长度之和。而在“联合”中,各成员共享一段内存空间, 一个联合变量

大语言模型(LLMs)能够进行推理和规划吗?

大语言模型(LLMs),基本上是经过强化训练的 n-gram 模型,它们在网络规模的语言语料库(实际上,可以说是我们文明的知识库)上进行了训练,展现出了一种超乎预期的语言行为,引发了我们的广泛关注。从训练和操作的角度来看,LLMs 可以被认为是一种巨大的、非真实的记忆库,相当于为我们所有人提供了一个外部的系统 1(见图 1)。然而,它们表面上的多功能性让许多研究者好奇,这些模型是否也能在通常需要系

回调的简单理解

之前一直不太明白回调的用法,现在简单的理解下 就按这张slidingmenu来说,主界面为Activity界面,而旁边的菜单为fragment界面。1.现在通过主界面的slidingmenu按钮来点开旁边的菜单功能并且选中”区县“选项(到这里就可以理解为A类调用B类里面的c方法)。2.通过触发“区县”的选项使得主界面跳转到“区县”相关的新闻列表界面中(到这里就可以理解为B类调用A类中的d方法

Linux系统稳定性的奥秘:探究其背后的机制与哲学

在计算机操作系统的世界里,Linux以其卓越的稳定性和可靠性著称,成为服务器、嵌入式系统乃至个人电脑用户的首选。那么,是什么造就了Linux如此之高的稳定性呢?本文将深入解析Linux系统稳定性的几个关键因素,揭示其背后的技术哲学与实践。 1. 开源协作的力量Linux是一个开源项目,意味着任何人都可以查看、修改和贡献其源代码。这种开放性吸引了全球成千上万的开发者参与到内核的维护与优化中,形成了

人工和AI大语言模型成本对比 ai语音模型

这里既有AI,又有生活大道理,无数渺小的思考填满了一生。 上一专题搭建了一套GMM-HMM系统,来识别连续0123456789的英文语音。 但若不是仅针对数字,而是所有普通词汇,可能达到十几万个词,解码过程将非常复杂,识别结果组合太多,识别结果不会理想。因此只有声学模型是完全不够的,需要引入语言模型来约束识别结果。让“今天天气很好”的概率高于“今天天汽很好”的概率,得到声学模型概率高,又符合表达

Spring中事务的传播机制

一、前言 首先事务传播机制解决了什么问题 Spring 事务传播机制是包含多个事务的方法在相互调用时,事务是如何在这些方法间传播的。 事务的传播级别有 7 个,支持当前事务的:REQUIRED、SUPPORTS、MANDATORY; 不支持当前事务的:REQUIRES_NEW、NOT_SUPPORTED、NEVER,以及嵌套事务 NESTED,其中 REQUIRED 是默认的事务传播级别。

C语言 将“China”译成密码

将“China”译成密码,密码规律是:用原来的字母后面的第4个字母代替原来的字母。例如,字母“A”后面的第4个字母是“E”,用“E”代替“A”。因此,“China”应译为“Glmre”。编译程序用付赋初值的方法使c1,c2,c3,c4,c5这五个变量的值分别为“C”,“h”,“i”,“n”,“a”,经过运算,使c1,c2,c3,c4,c5分别变成“G”,“l”,“m”,“r”,“e”。分别用put

C语言入门系列:探秘二级指针与多级指针的奇妙世界

文章目录 一,指针的回忆杀1,指针的概念2,指针的声明和赋值3,指针的使用3.1 直接给指针变量赋值3.2 通过*运算符读写指针指向的内存3.2.1 读3.2.2 写 二,二级指针详解1,定义2,示例说明3,二级指针与一级指针、普通变量的关系3.1,与一级指针的关系3.2,与普通变量的关系,示例说明 4,二级指针的常见用途5,二级指针扩展到多级指针 小结 C语言的学习之旅中,二级

如何理解redis是单线程的

写在文章开头 在面试时我们经常会问到这样一道题 你刚刚说redis是单线程的,那你能不能告诉我它是如何基于单个线程完成指令接收与连接接入的? 这时候我们经常会得到沉默,所以对于这道题,笔者会直接通过3.0.0源码分析的角度来剖析一下redis单线程的设计与实现。 Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源

MySQL理解-下载-安装

MySQL理解: mysql:是一种关系型数据库管理系统。 下载: 进入官网MySQLhttps://www.mysql.com/  找到download 滑动到最下方:有一个开源社区版的链接地址: 然后就下载完成了 安装: 双击: 一直next 一直next这一步: 一直next到这里: 等待加载完成: 一直下一步到这里