当前位置: 首页 > news >正文

深入理解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)
}

相关文章:

  • 电压互感器在线监测的原理
  • AI 大模型企业应用实战(10)-LLMs和Chat Models
  • AI大模型企业应用实战(14)-langchain的Embedding
  • Qt异常处理
  • 软件测试过程中用接口怎么将web系统的多页数据展示在1页
  • 罗盘时钟lua迷你世界
  • Parallels Desktop 19 for mac破解版安装激活使用指南
  • ArcgisEngine 释放内存案例
  • React 中的 ErrorBoundary
  • 数据分析:置换检验Permutation Test
  • 【JavaEE】Spring Web MVC详解
  • 【ajax核心05】宏任务与微任务
  • 【计算机网络】已解决:“‘ping‘ 不是内部或外部命令,也不是可运行的程序或批处理文件”报错
  • 2-14 基于matlab的GA优化算法优化车间调度问题
  • PMBOK® 第六版 管理项目知识
  • 【Linux系统编程】快速查找errno错误码信息
  • Debian下无root权限使用Python访问Oracle
  • Effective Java 笔记(一)
  • ES6--对象的扩展
  • Flannel解读
  • Iterator 和 for...of 循环
  • jdbc就是这么简单
  • LeetCode算法系列_0891_子序列宽度之和
  • Linux快速复制或删除大量小文件
  • node 版本过低
  • orm2 中文文档 3.1 模型属性
  • Python - 闭包Closure
  • python 学习笔记 - Queue Pipes,进程间通讯
  • 安卓应用性能调试和优化经验分享
  • 分享一个自己写的基于canvas的原生js图片爆炸插件
  • 聊聊sentinel的DegradeSlot
  • 树莓派 - 使用须知
  • 微信小程序开发问题汇总
  • 详解移动APP与web APP的区别
  • media数据库操作,可以进行增删改查,实现回收站,隐私照片功能 SharedPreferences存储地址:
  • ​Linux·i2c驱动架构​
  • # Panda3d 碰撞检测系统介绍
  • # 利刃出鞘_Tomcat 核心原理解析(八)-- Tomcat 集群
  • #HarmonyOS:软件安装window和mac预览Hello World
  • #单片机(TB6600驱动42步进电机)
  • #数据结构 笔记三
  • $().each和$.each的区别
  • (12)Linux 常见的三种进程状态
  • (C++)栈的链式存储结构(出栈、入栈、判空、遍历、销毁)(数据结构与算法)
  • (JSP)EL——优化登录界面,获取对象,获取数据
  • (Mac上)使用Python进行matplotlib 画图时,中文显示不出来
  • (分布式缓存)Redis分片集群
  • (接口自动化)Python3操作MySQL数据库
  • (免费领源码)Java#Springboot#mysql农产品销售管理系统47627-计算机毕业设计项目选题推荐
  • (三)Honghu Cloud云架构一定时调度平台
  • (原創) 物件導向與老子思想 (OO)
  • (转载)(官方)UE4--图像编程----着色器开发
  • (转载)VS2010/MFC编程入门之三十四(菜单:VS2010菜单资源详解)
  • (最新)华为 2024 届秋招-硬件技术工程师-单板硬件开发—机试题—(共12套)(每套四十题)
  • .bat批处理(一):@echo off