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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/32868.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何在Java中进行网络编程?

如何在Java中进行网络编程&#xff1f; 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天我们将探讨如何在Java中进行网络编程&#xff0c;这是实现客户端和服…

Vite: 关于静态资源的处理机制

概述 随着前端技术的飞速发展&#xff0c;项目规模和复杂度不断增加&#xff0c;如何高效地处理静态资源成为了提升开发效率和应用性能的关键Vite&#xff0c;作为新一代前端构建工具&#xff0c;以其轻量级、快速启动和热更新著称&#xff0c;同时也为静态资源的管理和优化提…

使用 axios 进行 HTTP 请求

使用 axios 进行 HTTP 请求 文章目录 使用 axios 进行 HTTP 请求1、介绍2、安装和引入3、axios 基本使用4、axios 发送 GET 请求5、axios 发送 POST 请求6、高级使用7、总结 1、介绍 什么是 axios axios 是一个基于 promise 的 HTTP 库&#xff0c;可以用于浏览器和 Node.js 中…

计算机组成入门知识

前言&#x1f440;~ 数据库的知识点先暂且分享到这&#xff0c;接下来开始接触计算机组成以及计算机网络相关的知识点&#xff0c;这一章先介绍一些基础的计算机组成知识 一台计算机如何组成的&#xff1f; 存储器 CPU cpu的工作流程 主频 如何衡量CPU好坏呢&#xff1f…

创意产业如何应对AI的挑战。

最近的一个月&#xff0c;音乐领域迎来了一个革命性的变化。一系列音乐大模型轮番上线&#xff0c;它们以惊人的创作能力&#xff0c;将素人生产音乐的门槛降到了最低。这些AI音乐模型的出现&#xff0c;引发了关于AI是否会彻底颠覆音乐圈的讨论。然而&#xff0c;短暂的兴奋过…

我的常见问题记录

1,maven在idea工具可以正常使用,在命令窗口执行出现问题 代码: E:\test-hello\simple-test>mvn clean compile [INFO] Scanning for projects... [WARNING] [WARNING] Some problems were encountered while building the effective model for org.consola:simple-test:jar…

【从0实现React18】 (三) 初探reconciler 带你初步探寻React的核心逻辑

Reconciler 使React核心逻辑所在的模块&#xff0c;中文名叫协调器&#xff0c;协调(reconciler)就是diff算法的意思 reconciler有什么用&#xff1f; 在前端框架出现之前&#xff0c;通常会使用 jQuery 这样的库来开发页面。jQuery 是一个过程驱动的库&#xff0c;开发者需要…

【windows解压】解压文件名乱码

windows解压&#xff0c;文件名乱码但内容正常。 我也不知道什么时候设置出的问题。。。换了解压工具也没用&#xff0c;后来是这样解决的。 目录 1.环境和工具 2.打开【控制面板】 3.点击【时钟和区域】 4.选择【区域】 5.【管理】中【更改系统区域设置】 6.选择并确定…

算是一些Transformer学习当中的重点内容

一、基础概念 Transformer是一种神经网络结构&#xff0c;由Vaswani等人在2017年的论文Attentions All YouNeed”中提出&#xff0c;用于处理机器翻译、语言建模和文本生成等自然语言处理任务。Transformer同样是encoder-decoder的结构&#xff0c;只不过这里的“encoder”和“…

设计模式——工厂方法模式

文章目录 工厂方法模式简介工厂方法模式的组成部分工厂方法模式的结构Factory和Method的含义工厂方法模式的应用场景工厂方法模式的示例1. 文档生成器2. 数据库连接 工厂方法模式简介 工厂方法模式&#xff08;Factory Method Pattern&#xff09;是一种创建型设计模式&#x…

安卓设备屏幕分辨率适配

需求 适配1.83寸的手表屏幕 屏幕分辨率为240px*284px dpi203.18 计量单位 px &#xff08;Pixels&#xff09;&#xff1a; 其实就是像素单位&#xff0c;是屏幕上最小可见的单元&#xff0c;比如我们通常说的手机分辨列表800*400都是px的单位&#xff08;px 的单位是 像素&am…

完美解决找不到steam_api64.dll无法执行代码问题

游戏缺失steam_api64.dll通常意味着该游戏依赖于Steam平台的一些功能或服务&#xff0c;而这个DLL文件是Steam客户端的一部分&#xff0c;用于游戏与Steam平台之间的交互。如果游戏中缺失这个文件&#xff0c;可能会出现无法启动、崩溃或其他问题。 一&#xff0c;详细了解stea…

第13关:存储过程1、第14关:存储过程2。(2021数据库期末一)

目录 首先需要学习和了解的知识 第13关&#xff1a;存储过程1 任务描述 答案 第14关&#xff1a;存储过程2 任务描述 答案 本篇博客的答案博主是学习别人得来的&#xff0c;敢于借鉴和学习哈哈&#xff01;&#xff01; 首先需要学习和了解的知识 了解什么是存储过程以及…

针对ARM64嵌入式系统的Linux内核参数优化

文章目录 0. 概要1. 网络性能优化开启TCP连接重用减少TCP连接超时时间 2. 文件系统和I/O优化提高文件描述符限制 3. 内存管理优化提高内存可用性 4. 内核调度优化调整CFS调度器的调度周期 5. 完整配置文件 0. 概要 在ARM64架构的嵌入式系统中&#xff0c;系统性能和资源优化至…

JeecgBoot v3.7.0 all 版本发布,前后端合并一个仓库

项目介绍 JeecgBoot是一款企业级的低代码平台&#xff01;前后端分离架构 SpringBoot2.x&#xff0c;SpringCloud&#xff0c;Ant Design&Vue3&#xff0c;Mybatis-plus&#xff0c;Shiro&#xff0c;JWT 支持微服务。强大的代码生成器让前后端代码一键生成! JeecgBoot引领…

音频——性能测试中的基本概念

文章目录 频率响应平均电平增益ADC 路径增益DAC 路径增益底噪信噪比总谐波失真+噪声(THD+N)延迟频率响应 对于音频设备,频率响应可以理解为音频设备对不同频率信号的处理或重现。对于音频信号频率,一般关注20Hz~20kHz范围。理想情况下,输入幅度相同的不同频率信号,过音频…

吴恩达机器学习 第二课 week4 决策树

目录 01 学习目标 02 实现工具 03 问题描述 04 构建决策树 05 总结 01 学习目标 &#xff08;1&#xff09;理解“熵”、“交叉熵&#xff08;信息增益&#xff09;”的概念 &#xff08;2&#xff09;掌握决策树的构建步骤与要点 02 实现工具 &#xff08;1&#xff09;…

常见的七大排序

目录 前言 冒泡排序 选择排序 插入排序 堆排序 希尔排序 快排 归并排序 前言 本文介绍七种常见的排序方式&#xff1a;冒泡排序&#xff0c;选择排序&#xff0c;插入排序&#xff0c;堆排序&#xff0c;希尔排序&#xff0c;快排&#xff0c;归并排序 冒泡排序 将每2…

Linux使用——查看发行版本、内核、shell类型等基本命令

先做快照 虚拟机中编辑网络 关机 普通账户和管理员账户 互相对照 localhost相当于IP 参数: 短格式:以减号(-)开头&#xff0c;参数字母 长格式:以2个减号(--)后跟上完整的参数单词 当前发行版本 [rootserver ~]# cat /etc/redhat-release Red Hat Enterprise Linux release 9.…