「GO标准库」reflect 包的全面解析

什么是反射?

Java语言一样,Go语言也有运行时反射,这为我们提供了一种可以在运行时操作任意类型对象的能力。比如查看一个接口变量的具体类型、看看一个结构体有多少字段、修改某个字段的值等。Go语言是静态编译类语言,比如在定义一个变量的时候,已经知道了它是什么类型,那么为什么还需要反射呢?这是因为有些事情只有在运行时才知道。比如你定义了一个函数,它有一个interface{}类型的参数,这也就意味着调用者可以传递任何类型的参数给这个函数。在这种情况下,如果你想知道调用者传递的是什么类型的参数,就需要用到反射。如果你想知道一个结构体有哪些字段和方法,也需要反射。

获取对象的值和类型

Go语言的反射定义中,任何接口都由两部分组成:接口的具体类型,以及具体类型对应的值。比如var i int=3,因为interface{}可以表示任何类型,所以变量i可以转为interface{}。你可以把变量i当成一个接口,那么这个变量在Go反射中的表示就是<Value,Type>。其中Value为变量的值,即3,而Type为变量的类型,即int

提示:

interface{}是空接口,可以表示任何类型,也就是说你可以把任何类型转换为空接口,它通常用于反射、类型断言,以减少重复代码,简化编程。

Go反射中,标准库为我们提供了两种类型reflect.Valuereflect.Type来分别表示变量的值和类型,并且提供了两个函数reflect.ValueOfreflect.TypeOf分别获取任意对象的reflect.Valuereflect.Type

go 复制代码
package main
import(
    "fmt"
    "reflect"
)

func main(){
    i := 3
    iv := reflect.ValueOf(i)
    it := reflect.TypeOf(i)
    
    fmt.Println(iv, it)
    // 3  int
}

代码定义了一个int类型的变量i,它的值为3,然后通过reflect.ValueOfreflect.TypeOf函数就可以获得变量i对应的reflect.Valuereflect.Type。通过fmt.Println函数打印后,可以看到结果是"3 int",这也可以证明reflect.Value表示的是变量的值,reflect.Type表示的是变量的类型。

reflect.Value

reflect.Value可以通过函数reflect.ValueOf获得,下面将为你介绍它的结构和用法。

reflect.Value 结构体定义

Go语言中,reflect.Value被定义为一个结构体,它的定义如下面的代码所示:

go 复制代码
type Value struct {
    //typ_ 保存由 value 表示的值的类型。
    //使用 typ 方法访问以避免 v 的转义。
    typ_ *abi.Type

    //指针值数据,或者,如果设置了 flagIndir,则为指向数据的指针。
    //当 flagIndir 被设置或 typ.pointers()为true时有效。
    ptr unsafe.Pointer

    //标志保存有关该值的元数据。
    //最低的五位给出值的种类,镜像类型。Kind()。
    //下一组位是标志位:
    //-flagStickyRO:通过未过期未嵌入字段获取,因此为只读
    //-flagEmbedRO: 通过未导出的嵌入字段获取,因此为只读
    //-flagIndir:   val持有指向数据的指针
    //-flagAddr:    v.CanAddr为true(表示flagIndir和ptr为非零)
    //-flagMethod:  v是一个方法值。
    //如果ifaceIndir(typ),代码可以假设flagIndir已设置。
    //剩下的22+位给出了方法值的方法编号。
    //If flag.kind()!=Func,代码可以假设flagMethod是未设置的。
    flag

    //方法值表示当前的方法调用
    //类似r。读取某些接收器r。典型值+val+标志位描述
    //接收器r,但标志的Kind位表示Func(方法为
    //函数),并且标志的顶部位给出方法编号
    //在r的类型的方法表中。
}

type flag uintptr

我们发现reflect.Value结构体的字段都是私有的,也就是说,我们只能使用reflect.Value的方法。现在看看它有哪些常用方法,如下所示:

  • 针对具体类型的系列方法
go 复制代码
// 用户获取对应的值
Bool()
Bytes()
Complex()
Float()
Int()
String()
Uint()
CanAddr()  // 是否可以使用Addr获取值的地址
CanSet()   // 是否可以修改对应的值

// 用户修改对应的值
Set()
SetBool()
SetBytes()
SetComplex()
SetFloat()
SetInt()
SetString()



Elem() // 获取指针指向的值,一般用于修改对应的值
  • 针对 struct 类型的系列方法
go 复制代码
// Field 系列方法用于获取 struct 类型中的字段
Filed()
FiledByIndex()
FiledByName()
FiledByNameFunc()

Interface() // 获取对应的原始类型
IsNil()     // 值是否为nil
IsZero()    // 值是否是零值
Kind()      // 获取对应的类型类别,比如 Array、Slice、Map 等
  • 获取类型上的方法集
go 复制代码
// 获取对应的方法
Method()
MethodByName()

NumField()   //获取 struct 类型中字段的数量
NumMethod()  // 获取类型上方法集的数量
Type()       // 获取对应的 reflect.Type

看着比较多,其实就三类:

  • 一类用于获取和修改对应的值;
  • 一类与struct类型的字段有关,用于获取对应的字段;
  • 一类与类型上的方法集有关,用于获取对应的方法。

reflect.Type

reflect.Value可以用于与值有关的操作,而如果是与变量类型本身有关的操作,比如要获取结构体对应的字段名称或方法,则最好使用reflect.Type

reflect.Type 接口定义

reflect.Value不同,reflect.Type是一个接口,而不是一个结构体,所以也只能使用它的方法。

go 复制代码
type Type interface {

    // 返回值的对齐方式(以字节为单位)
    // 用作在内存中分配时
    Align() int
    
    // 返回值的对齐方式(以字节为单位)
    // 用作结构中的字段
    FieldAlign() int
    
    // 返回已定义类型的包中的类型名称
    Name() string
    
    // 返回定义类型的包路径,即导入路径
    PkgPath() string
    
    // 返回存储所需的字节数
    Size() uintptr
    
    // 返回该类型的字符串表示形式
    String() string

    Implements(u Type) bool
    AssignableTo(u Type) bool
    ConvertibleTo(u Type) bool
    Comparable() bool
    
    // 以位为单位返回类型的大小
    Bits() int
    
    // 返回通道类型的方向
    ChanDir() ChanDir
    
    // 报告函数类型的最终输入参数
    IsVariadic() bool
    
    // 返回函数类型的第i个输入参数的类型
    In(i int) Type
    
    // 返回映射类型的键类型
    Key() Type
    
    // 返回数组类型的长度
    Len() int
    
    // 返回函数类型的输入参数计数
    NumIn() int
    
    // 返回函数类型的输出参数计数
    NumOut() int
    
    // 返回函数类型的第i个输出参数的类型
    Out(i int) Type

    // 以下这些方法与 Value 结构体的功能相同
    Kind() Kind
    Method(int) Method
    MethodByName(string) (Method, bool)
    NumMethod() int

    Elem() Type
    Field(i int) StructField
    FieldByIndex(index []int) StructField
    FieldByName(name string) (StructField, bool)
    FieldByNameFunc(match func(string) bool) (StructField, bool)
    NumField() int
}

其中有几个特有的方法:

  • Implements 用于判断是否实现了接口 u
  • AssignableTo 用于判断是否可以使用"=",即赋值运算符赋值给类型 u
  • ConvertibleTo 用于判断是否可以转换成类型 u,其实就是是否可以进行类型转换
  • Comparable 用于判断该类型是否可以使用关系运算符进行比较

小技巧

你可以通过FieldByName方法获取指定的字段,也可以通过MethodByName方法获取指定的方法,这在需要获取某个特定的字段或者方法而不是遍历时非常高效。

是否实现某接口?

通过 reflect.Type 中的Implements 可以判断是否实现了某接口。

go 复制代码
package main

import (
    "fmt"
    "io"
    "reflect"
)

type persion struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

// 为 persion 增加一个方法 String ,返回对应的字符串信息
// 这样 persion 结构体就实现了 fmt.Stringer 接口
func (p persion) String() string {
    return fmt.Sprintf("Name is %s, Age is %d", p.Name, p.Age)
}

func main() {
    p := persion{
       Name: "码一行",
       Age:  26,
    }

    pt := reflect.TypeOf(p)

    stringerType := reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
    writerType := reflect.TypeOf((*io.Writer)(nil)).Elem()
    fmt.Println("是否实现了 fmt.Stringer: ", pt.Implements(stringerType))
    fmt.Println("是否实现了 io.Writer: ", pt.Implements(writerType))
}

提示

尽可能通过类型断言的方式判断是否实现了某接口,而不是通过反射

这个示例通过Implements方法来判断是否实现了fmt.Stringerio.Writer接口, 运行结果:

go 复制代码
是否实现了 fmt.Stringer:  true
是否实现了 io.Writer:  false

因为结构体person只实现了fmt.Stringer接口,没有实现io.Writer接口,所以与验证的结果一致。

反射定律

反射是计算机语言中程序检视其自身结构的一种方法,它属于元编程的一种形式。反射灵活、强大,但也存在不安全因素。它可以绕过编译器的很多静态检查,如果过多使用便会造成混乱。为了帮助开发者更好地理解反射,Go语言的作者在博客上总结了反射的三大定律。

  1. 任何接口值interface{}都可以反射出反射对象,也就是reflect.Valuereflect.Type通过函数reflect.ValueOfreflect.TypeOf获得。
  2. 反射对象也可以还原为interface{}变量,也就是第1条定律可逆性,通过reflect.Value结构体的Interface方法获得。
  3. 要修改反射的对象,该值必须可设置,也就是可寻址

提示

任何类型的变量都可以转换为空接口intferface{}

所以第1条定律中函数reflect.ValueOfreflect.TypeOf的参数就是interface{},表示可以把任何类型的变量转换为反射对象。

在第2条定律中,reflect.Value结构体的Interface方法返回的值也是interface{},表示可以把反射对象还原为对应的类型变量。

一旦你理解了这三大定律,就可以更好地理解和使用Go语言反射。

结束语

在反射中,reflect.Value对应的是变量的值,如果你需要进行与变量的值有关的操作,应该优先使用reflect.Value,比如获取变量的值、修改变量的值等。reflect.Type对应的是变量的类型,如果你需要进行与变量的类型本身有关的操作,应该优先使用reflect.Type,比如获取结构体内的字段、类型拥有的方法集等。

此外我要再次强调:反射虽然很强大,可以简化编程、减少重复代码,但是过度使用会让你的代码变得复杂混乱。所以除非非常必要,否则尽可能少地使用它们。

相关推荐
lypzcgf24 分钟前
Coze源码分析-资源库-编辑工作流-后端源码-流程/技术/总结
go·源码分析·工作流·coze·coze源码分析·ai应用平台·agent平台
小蒜学长1 小时前
springboot多功能智能手机阅读APP设计与实现(代码+数据库+LW)
java·spring boot·后端·智能手机
追逐时光者2 小时前
精选 4 款开源免费、美观实用的 MAUI UI 组件库,助力轻松构建美观且功能丰富的应用程序!
后端·.net
你的人类朋友3 小时前
【Docker】说说卷挂载与绑定挂载
后端·docker·容器
间彧3 小时前
在高并发场景下,如何平衡QPS和TPS的监控资源消耗?
后端
间彧3 小时前
QPS和TPS的区别,在实际项目中,如何准确测量和监控QPS和TPS?
后端
间彧4 小时前
消息队列(RocketMQ、RabbitMQ、Kafka、ActiveMQ)对比与选型指南
后端·消息队列
brzhang5 小时前
AI Agent 干不好活,不是它笨,告诉你一个残忍的现实,是你给他的工具太难用了
前端·后端·架构
brzhang5 小时前
一文说明白为什么现在 AI Agent 都把重点放在上下文工程(context engineering)上?
前端·后端·架构
Roye_ack5 小时前
【项目实战 Day9】springboot + vue 苍穹外卖系统(用户端订单模块 + 商家端订单管理模块 完结)
java·vue.js·spring boot·后端·mybatis