首页 > 编程笔记 > Go语言笔记 阅读:17

Go语言reflect反射包的用法(非常详细)

Go 语言标准库的反射包 reflect 主要有两个函数 reflect.TypeOf() 和 reflect.ValueOf()。与反射相关的任务基本上都是先通过这两个函数来获得反射对象的两大要素 reflect.Type 接口和 reflect.Value 结构体类型,然后再使用反射包中的其他方法继续下一步操作。

以 reflect.Type 接口为例,它提供的 MethodByName() 方法用来获取当前类型对应方法的引用,Implements() 方法用来判断当前类型是否实现了接口。在 Go 语言中,reflect.Value 可以保存任意类型变量的存储结构,因此,可以直接将函数 reflect.ValueOf() 的返回值作为变量值使用。

笔者针对反射包 reflect 中常用的对象和方法进行了归纳总结,如下图所示:


图 1 反射包 reflect 中常用的对象和方法

从图 1 可以看出,从接口值到反射对象需要经历两次转换。第一次是将基本类型转换为接口类型,第二次是将接口类型转换为反射对象。从反射对象到接口值是上述过程的反向过程。

Go语言反射对象的转换机制

前面已提到,反射对象的两大要素为 reflect.Type 接口和 reflect.Value 结构体类型,接下来看看与之相关的转换机制。

1) 将任意值转换为reflect.Type接口类型的值

函数 reflect.TypeOf() 用于动态获取任意值的类型信息,而不仅限于接口类型的值。这个函数接收 interface{} 类型的参数,可以接收任意类型的值,因为在 Go语言中,任何值都可以被视为 interface{} 类型。

调用函数 reflect.TypeOf() 最后返回的是一个 reflect.Type 接口类型的值,它表示传入值的类型。如果传入的值为 nil 则返回 nil,这是因为 nil 接口值没有具体的动态类型。

关键代码如下:
func TypeOf(i interface{}) Type {
    eface := *(*emptyInterface)(unsafe .Pointer(&i))
    return toType(eface .typ)
}
 
type emptyInterface struct {
   typ  *rtype
   word unsafe .Pointer
}
上述代码中的 emptyInterface 就是 Go 源码 runtime 2.go 中空接口的定义 eface,eface.typ 则是动态类型,返回值的类型是 reflect.Type 接口类型,其具体用法可以参考官方文档。

2) 将任意值转换为reflect.Value结构体类型的值

函数 reflect.ValueOf() 用于获取任何值的类型信息,与函数 reflect.TypeOf() 一样,如果传入的值为 nil 则返回 nil。

调用函数 reflect.ValueOf() 返回的值包含了原始值及其类型信息的反射对象。这个函数广泛用于获取和操作运行时的值信息,是 Go 语言反射机制的核心部分。

在源码中,接口类型实例转换为 reflect.Value 结构体类型的过程是先将 i 转换成 *emptynterace 类型,再将它的字段 typ、word 和一个标志位组装成一个 Value 结构体,然后以此作为函数 reflect.ValueOf() 的返回值。关键代码如下:
func ValueOf(i interface{}) Va与ue {
     if i == nil {
          return Value{}
     }
     ...
    return unpackEface(i)
}
 
func unpackEface(i interface{}) Value {
     e := (*empt Interface)(unsafe.Pointer(&i))
     t := e.typ
     f := flag(t.Kind())
     ...
     return Value{t, e.word, f}
}

Go语言reflect.Type接口的转换方式

从图 1 中可以看到 reflect.Type 接口的转换方式有两种:

1) 将reflect.Type接口转换为reflect.Value结构体类型

我们无法直接将 reflect.Type 接口转换为 reflect.Value 结构体类型,这是因为 reflect.Type 接口中仅有类型信息,没有具体的值信息。办法是通过 reflect.Type 接口构建新的接口类型实例,然后为其赋予零值并返回。

reflect.New() 函数可以根据给定的类型创建一个新的指针,它适用于需要创建一个指向某个类型零值的指针的场景。reflect.New() 函数返回指向这个新的零值的 reflect.Value 结构体类型。示例代码如下:
func TestTypeNew(s *testing.T) {
    t := reflect.TypeOf(1024)   // 获取int的reflect.Type接口
    v := reflect.New(t)         // 创建 *int的reflect.Value结构体类型(指向int的零值)
    fmt.Println(v.Elem().Int()) // 输出0,因为它是int的零值
}

reflect.Zero() 函数可以根据给定的类型创建该类型的零值,并返回这个零值的 reflect.Value 结构体类型。这适用于不需要指针,直接需要类型零值的场景。

如果知道类型值的存储地址,则可以用 NewAt() 函数恢复 reflect.Value 结构体类型,示例代码如下:
var x float64 = 3.14   
t := reflect.TypeOf(x) // 获取x的类型信息   
ptr := unsafe.Pointer(&x) // 获取x的内存地址
v := reflect.NewAt(t, ptr) // 使用NewAt根据类型和内存地址恢复reflect.Value结构体类型
fmt.Println("Value:", v.Elem().Float()) // 使用Elem获取Value指向的实际值,输出3.14
在上面的示例中:

2) 值类型reflect.Type接口与指针类型reflect.Type接口互转

将值类型 reflect.Type 接口转换为指针类型 reflect.Type 接口时使用 PtrTo() 方法,将指针类型 reflect.Type 接口转换为值类型 reflect.Type 接口时使用 Elem() 方法。

在 Go 语言的反射库中,Elem() 方法用于获取一个指针、数组、切片、映射和通道的基础(底层)类型,除此之外的其他类型使用 Elem() 方法时会发生 panic。PtrTo() 和 Elem() 方法是反射机制中处理类型信息时使用的互补方法,二者提供了一种在运行时动态探索和变换 Go 类型的能力,这在要动态处理不同类型的数据时特别有用。

示例代码如下:
// reflect.Type接口 的elem方法说明
func TestTypeElem(t *testing .T) {
     s := 1
     var intPtr = &s
     mySlice := []string{"Oracle", "MySQL", "PgSQL"}
     myMap := map[string]int{"Java": 1, "Go": 2}
     myArray := [ . . .]string{}
     var myChan chan int = make(chan int)
 
     intPtrKind := reflect .TypeOf(intPtr) .Elem()
     mySliceKind := reflect .TypeOf(mySlice) .Elem()
     myMapKind := reflect .TypeOf(myMap) .Elem()
     myArrayKind := reflect .TypeOf(myArray) .Elem()
     myChanKind := reflect .TypeOf(myChan) .Elem()
 
     fmt .Printf("intPtr .Elem():%s\n", intPtrKind)//输出: intPtr .Elem():int
     fmt .Printf("mySlice .Elem():%s\n", mySliceKind)//输出:mySlice .Elem():string
     fmt .Printf("myMap .Elem():%s\n", myMapKind)//输出: myMap .Elem():int
     fmt .Printf("myArray .Elem():%s\n", myArrayKind)//输出:myArray .Elem():string
     fmt .Printf("myChan .Elem():%s\n", myChanKind)//输出: myChan .Elem():int
}
从上述代码的输出结果中可以看到如下信息:

Go语言reflect.Value结构体的使用方法

下面来看看 reflect.Value 结构体类型是如何转换为原始的接口类型和获取 reflect.Type 接口的。

1) 转换为原始的接口类型

reflect.Value 结构体类型本身就包含类型和值信息,因此能很轻松地转换为接口类型。示例代码如下:
func main() {
    //结构体的反射
    v := MyStruct{}
    value := reflect .ValueOf(v)
    fmt .Printf("Kind : %s , Type : %s\n", value .Kind(), value .Type())
}
type MyStruct struct {}
//输出: Kind : struct , Type : main .MyStruct

2) 将已知的原有类型转换为具体类型

reflect.Value 结构体提供了一系列方法可直接将 reflect.Value 结构体类型转换为 Go 语言中的具体类型,例如 Int、Uint、Float、Bool 等。这些方法对应着各种基本数据类型,它们允许我们从 reflect.Value 结构体类型中提取其表示的原始值,前提是这个值确实是对应的类型。

例如:
v := reflect .ValueOf(42) // v是一个reflect.Value结构体类型
i := v .Int() // 使用Int方法将v转换为int64类型,i的值为42
但是这些转换方法只在 reflect.Value 结构体类型表示的值与方法类型匹配时才可以使用,如果两者不匹配,运行时会产生 panic。

例如,如果 v 表示一个字符串,那么尝试调用 v.Int 方法就会产生运行时 panic。因此,在调用这些转换方法前,通常需要检查 reflect.Value 的结构体类型,可以使用 Kind() 方法进行检查。

另外要注意的是,转换时要区分反射的目标(reflect.ValueOf)是指针还是值!对于指针类型的 reflect.Value,需要先调用 Elem() 方法获取指针指向的实际值,然后再进行类型转换。

示例代码如下:
var x float64 = 3.14
v := reflect.ValueOf(x)
 
// 正确的使用方式
if v.Kind() == reflect.Float64 {
    fmt.Println("Float value:", v.Float())
}
 
// 错误的使用方式会导致产生panic
// fmt.Println("Int value:", v.Int())
 
// 处理指针类型
pv := reflect.ValueOf(&x)
if pv.Kind() == reflect.Ptr && pv.Elem().Kind() == reflect.Float64 {
    fmt.Println("Float value:", pv.Elem().Float())
}

3) 原有类型未知,进行探索式转换

很多情况下,我们是不知道原有类型的。此时,需要使用 Interface 方法进行探索式转换。

以下代码演示了将较为复杂的结构体类型从 reflect.Value 结构体类型转换为接口类型的过程。
//接受一个interface{}类型的参数obj,它可以是Go语言中任意类型的值。它使用反射来获取并打印有关参数obj的信息
func GetObjInfo(obj interface{})  {
    getType := reflect .TypeOf(obj)
    fmt .Println("获取的类型为 :", getType .Name())
 
    getValue := reflect .ValueOf(obj)
    fmt .Println("获取的值为:", getValue)
 
    //是struct类型时才继续获取参数obj的字段和方法
    if getType .Kind()!=reflect .Struct{
         return
    }
 
    // 获取方法字段
    // 先获取interface的reflect.Type接口,然后通过NumField进行遍历
    // 再获取reflect.Type接口的Field
    // 最后通过Field的Interface方法得到对应的value
    for i := 0; i < getType .NumField(); i++ {
        field := getType .Field(i)
        value := getValue .Field(i) .Interface()
        fmt .Printf("%s: %v = %v\n", field .Name, field .Type, value)
    }
 
    // 获取方法
    // 先获取interface的reflect.Type接口,然后通过NumMethod进行遍历
    for i := 0; i < getType .NumMethod(); i++ {
        m := getType .Method(i)
        fmt .Printf("%s: %v\n", m .Name, m .Type)
    }
}
 
type MyFloat float64
 
//定义一个结构体Database
type DataBase struct {
    DbName  string
    DbType  string
    DbIndex int
}
 
// 测试使用Value获取原始的类型对象
func TestValue2Object(t *testing .T) {
    var MyDatabase = DataBase{
        DbName:  "Oracle",
        DbType:  "rdbms",
        DbIndex: 0,
    }
    GetObjInfo(MyDatabase)
    fmt .Println("----")
    var i MyFloat = 6.4
    GetObjInfo(i)
    fmt .Println("----")
    GetObjInfo(1)
}
输出结果为:

获取的类型为 : DataBase
获取的值为 : {Oracle rdbms 0}
DbName: string = Oracle
DbType: string = rdbms
DbIndex: int = 0
ToString: func(__reflect .DataBase, . . .string)
----
获取的类型为 : MyFloat
获取的值为 : 6.4
----
获取的类型为 : int
获取的值为 : 1


下面基于上面的示例总结一下获取结构体类型的成员类型和成员值,以及结构体类型相关方法的步骤。

获取结构体类型的成员类型和成员值的步骤如下:
获取结构体类型相关方法的步骤如下:

4) 将reflect.Value结构体类型转换为reflect.Type接口

因为每个 reflect.Value 结构体类型内部都包含一个指向对应类型信息的指针,所以可以直接调用方法 func(v Value) Type() Type 将 reflect.Value 结构体类型转换为对应的 reflect.Type 接口。这个 func(v Value) Type() Type 方法返回一个 reflect.Type 接口实例,它表示 reflect.Value 结构体类型所持有值的类型。

这种将 reflect.Value 结构体类型转换为 reflect.Type 接口的能力在需要动态处理数据类型的情况下非常有用,比如在序列化和反序列化、泛型编程或者编写依赖于类型检查的复杂算法时。

5) 如果reflect.Value结构体类型是指针类型,将其转换为值类型

在 Go 语言的反射库中,如果 reflect.Value 结构体类型是指针类型,有两种方法可以将其转换为值类型。

第一种,Elem() 方法用于获取一个指针或接口类型所指向或包含的值的 reflect.Value 结构体类型。其定义如下:
func (v Value) Elem() Value

示例代码如下:
var x int = 10
v := reflect.ValueOf(&x)
value := v.Elem() // value是指向x的指针所指向的int值的reflect.Value结构体类型

第二种,Indirect() 方法是 reflect 包提供的一个函数,如果 reflect.Value 结构体类型是指针类型,可用该函数获取其所指向的值类型,其定义如下:
func Indirect(v Value) Value
如果 v 是指针类型,函数返回指针值的 Value,否则返回 v 本身。示例代码如下:
var x int = 10
v := reflect.ValueOf(&x)
value := reflect.Indirect(v) // value是指向x的指针所指向的int值的reflect.Value结构体类型
使用 Elem() 方法需要先确保 reflect.Value 结构体类型是指针类型,否则可能会引发 panic。而使用 Indirect() 方法时不需要进行这样的前置检查,因为它在处理非指针类型时只是简单地返回原始的 reflect.Value 结构体类型。

Go语言reflect反射包的使用示例

reflect 反射提供了一种强大的机制以在运行时探索类型的结构和行为,接下来将演示反射包reflect中常用函数的使用示例。

首先,定义几个结构体并初始化一个全局的结构体变量,示例代码如下:
//定义一个结构体Database
type DataBase struct {
    DbName  string
    DbType  string
    DbIndex int
}
 
//定义Database这个结构体的ToString方法
func (db DataBase) ToString(args . . .string) {
    fmt .Printf("par=%s, DbName=%s, DbType=%s , DbIndex=%d\n",
    args, db .DbName, db .DbType, db .DbIndex)
}
 
//定义一个结构体Storage
type Storage struct {
    StorageType string `json:"name" bson:"Naming"`
    StorageSize float32 `json:"size" bson:"BigSize"`
}
 
//设置一个全局变量
var MyDatabase = DataBase{
    DbName:  "Oracle",
    DbType:  "rdbms",
    DbIndex: 0,
}

1) 获取变量的类型和值

接下来使用 reflect.TypeOf() 和 reflect.ValueOf() 函数获取变量的类型和值,以及传入接口的值的底层类型,示例代码如下:
// 反射的简单使用
func TestReflectBasicUse(test *testing .T) {
    //reflect .Value转换成了原来的对象
    obj_db := reflect .ValueOf(MyDataBase)
    obj := obj_db .Interface() . (DataBase)
 
    fmt .Printf("db的类型是%T:,值是%v\n", MyDataBase, MyDataBase)
    fmt .Printf("obj_db的类型是%T:,值是%v\n", obj_db, obj_db)
    fmt .Printf("obj的类型是%T:,值是%v\n", obj, obj)
 
    //%v+reflect .TypeOf(db) 等价于 %t+db
    fmt.Printf("db的类型是%v:,值是%v\n", reflect.TypeOf(MyDataBase), reflect.ValueOf
    (MyDataBase))
 
    //获取传入接口的底层原始数据结构
    //底层数据结构的种类可以参考type .go的const
    fmt .Println("底层的数据类型是", obj_db .Type() .Kind())
}
输出结果为:

db的类型是__reflect .DataBase:,值是{Oracle rdbms 0}
obj_db的类型是reflect .Value:,值是{Oracle rdbms 0}
obj的类型是__reflect .DataBase:,值是{Oracle rdbms 0}
db的类型是__reflect .DataBase:,值是{Oracle rdbms 0}
底层的数据类型是struct

代码说明如下:

2) 获取结构体的属性和方法

接下来看看获取结构体的属性和方法的示例,代码如下:
// 获取结构体的属性和方法
func TestGetStructPropsAndMethod(test *testing .T) {
    t := reflect .TypeOf(MyDatabase)
    for i := 0; i < t .NumField(); i++ {
            f := t .Field(i)
            fmt .Printf("fieldIndex: %d, fieldName: %s\n", f .Index, f .Name)
    }
 
    for i := 0; i < t .NumMethod(); i++ {
            m := t .Method(i)
            fmt .Printf("methodIndex: %d, methodName: %s\n", m .Index, m .Name)
    }
}
输出结果为:
fieldIndex: [0], fieldName: DbName
fieldIndex: [1], fieldName: DbType
fieldIndex: [2], fieldName: DbIndex
methodIndex: 0, methodName: ToString
上述代码主要通过 reflect.TypeOf(MyDatabase) 的方法 NumField 和方法 NumMethod 分别获取属性和方法。

3) 动态调用方法和传值

接下来演示如何动态调用方法和传值,示例代码如下:
func TestDynamicCallMethod(test *testing .T) {
    v := reflect .ValueOf(MyDataBase)
 
    methods := v .MethodByName("ToString")
    if methods .IsValid() {
        args := []reflect .Value{reflect .ValueOf("参数1"),
        reflect .ValueOf("参数2"), reflect .ValueOf("参数3")}
        fmt.Println(methods.Call(args))//输出:par=[参数1参数2参数3], DbName=Oracle,
        DbType=rdbms , DbIndex=0
    }
}
对于上述代码,有以下几点需要注意:
以上就是动态调用方法和传值的过程。因为函数还是一种数据类型,所以当以函数作为变量时,也可以使用反射进行操作。关键代码如下:
func fun1(){}
func fun2(i int, s string){}
 
value1 := reflect .ValueOf(fun1)
value2 := reflect .ValueOf(fun2)
 
value1 .Call(nil)
value2 .Call([]reflect .Value{reflect .ValueOf(100),reflect .ValueOf("hello")})

4) 修改接口值

我们可以通过反射机制来修改接口值。

首先,使用 reflect.ValueOf() 函数获取反射对象的 reflect.Value 结构体类型。其次,在修改接口值前,确保反射对象是可写的(settable)。这通常意味着原始变量应当是通过指针传递的。如果要修改的是结构体中的字段,可以使用 v.Elem().FieldByName("xxx") 来获取该字段的反射对象。最后,使用 Set、SetInt、SetString、SetBool 等方法来修改字段的值。

示例代码如下:
//通过反射机制修改接口值
//修改的原理是先获取reflect.Value结构体类型
//再通过v.Elem().FieldByName("xxx")来获取该字段的反射对象
//最后使用Set、SetInt、SetString、 SetBool等方法来修改字段的值
func TestModifyValueByReflect(test *testing .T) {
    //注意,这里传入的是指针
    myMySQL := &MyDataBase
    fmt .Println(myMySQL)//输出: &{Oracle rdbms 0}
    v := reflect .ValueOf(myMySQL) .Elem()
 
    v .FieldByName("DbName") .Set(reflect .ValueOf("MySQL"))
    fmt .Println(myMySQL)//输出: &{MySQL rdbms 0}
    fmt .Println(MyDataBase)//输出: {MySQL rdbms 0}
}
对于上述代码,需要注意的内容如下:
这里总结一下修改接口值时的注意事项:
如果要修改反射类型对象,其值必须是可写的(settable),这是反射的第三定律。

5) 判断结构体实现了哪个接口

除了类型断言和编译器自检,判断类型是否实现了接口还可以使用反射包提供的 reflect.TypeOf.Implements() 函数。

示例代码如下:
// implements reports whether the type V implements the interface type T .
func implements(T, V *rtype) bool
分析函数 implements 的算法,会发现它的算法时间复杂度是 O(m+n)而不是 O(m×n),这与通过接口中的 getitab 方法判断类型是否实现了接口的算法类似。

获取结构体的反射类型可以直接使用 reflect.Type 接口,但是要获取接口的类型就需要使用 reflect.TypeOf((*<interface>)(nil)).Elem 方法了。

下面这段代码演示了利用反射来判断结构体是否实现了接口的方法:
type coder interface {
    coding()
}
 
type Person struct {}
 
func (p *Person) coding() {}
 
func StructIsImplInterface(o interface{}, t reflect .Type) bool {
    obj := reflect .TypeOf(o)
    if obj .Implements(t) {
            return true
    }
    return false
}
 
//测试结构体是否实现了接口
func TestStructIsImplInterface(t *testing .T) {
    typeOfCoder := reflect .TypeOf((*coder)(nil)) .Elem()
    var person Person = Person{}
    fmt .Println(StructIsImplInterface(person, typeOfCoder))//输出: false
    fmt .Println(StructIsImplInterface(&person, typeOfCoder))//输出: true
}

相关文章