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

相关文章

深入理解Go语言中二维切片的使用

《深入理解Go语言中二维切片的使用》本文深入讲解了Go语言中二维切片的概念与应用,用于表示矩阵、表格等二维数据结构,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧... 目录引言二维切片的基本概念定义创建二维切片二维切片的操作访问元素修改元素遍历二维切片二维切片的动态调整追加行动态

Android ClassLoader加载机制详解

《AndroidClassLoader加载机制详解》Android的ClassLoader负责加载.dex文件,基于双亲委派模型,支持热修复和插件化,需注意类冲突、内存泄漏和兼容性问题,本文给大家介... 目录一、ClassLoader概述1.1 类加载的基本概念1.2 android与Java Class

java中反射Reflection的4个作用详解

《java中反射Reflection的4个作用详解》反射Reflection是Java等编程语言中的一个重要特性,它允许程序在运行时进行自我检查和对内部成员(如字段、方法、类等)的操作,本文将详细介绍... 目录作用1、在运行时判断任意一个对象所属的类作用2、在运行时构造任意一个类的对象作用3、在运行时判断

go中的时间处理过程

《go中的时间处理过程》:本文主要介绍go中的时间处理过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 获取当前时间2 获取当前时间戳3 获取当前时间的字符串格式4 相互转化4.1 时间戳转时间字符串 (int64 > string)4.2 时间字符串转时间

Go语言中make和new的区别及说明

《Go语言中make和new的区别及说明》:本文主要介绍Go语言中make和new的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 概述2 new 函数2.1 功能2.2 语法2.3 初始化案例3 make 函数3.1 功能3.2 语法3.3 初始化

Spring事务传播机制最佳实践

《Spring事务传播机制最佳实践》Spring的事务传播机制为我们提供了优雅的解决方案,本文将带您深入理解这一机制,掌握不同场景下的最佳实践,感兴趣的朋友一起看看吧... 目录1. 什么是事务传播行为2. Spring支持的七种事务传播行为2.1 REQUIRED(默认)2.2 SUPPORTS2

从原理到实战深入理解Java 断言assert

《从原理到实战深入理解Java断言assert》本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代... 目录深入理解 Java 断言(assert):从原理到实战引言:为什么需要断言?一、断言基础1.1 语

MySQL中的锁机制详解之全局锁,表级锁,行级锁

《MySQL中的锁机制详解之全局锁,表级锁,行级锁》MySQL锁机制通过全局、表级、行级锁控制并发,保障数据一致性与隔离性,全局锁适用于全库备份,表级锁适合读多写少场景,行级锁(InnoDB)实现高并... 目录一、锁机制基础:从并发问题到锁分类1.1 并发访问的三大问题1.2 锁的核心作用1.3 锁粒度分

Go语言中nil判断的注意事项(最新推荐)

《Go语言中nil判断的注意事项(最新推荐)》本文给大家介绍Go语言中nil判断的注意事项,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1.接口变量的特殊行为2.nil的合法类型3.nil值的实用行为4.自定义类型与nil5.反射判断nil6.函数返回的

Go语言数据库编程GORM 的基本使用详解

《Go语言数据库编程GORM的基本使用详解》GORM是Go语言流行的ORM框架,封装database/sql,支持自动迁移、关联、事务等,提供CRUD、条件查询、钩子函数、日志等功能,简化数据库操作... 目录一、安装与初始化1. 安装 GORM 及数据库驱动2. 建立数据库连接二、定义模型结构体三、自动迁