「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.Value
和reflect.Type
来分别表示变量的值和类型,并且提供了两个函数reflect.ValueOf
和reflect.TypeOf
分别获取任意对象的reflect.Value
和reflect.Type
。
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.ValueOf
和reflect.TypeOf
函数就可以获得变量i对应的reflect.Value
和reflect.Type
。通过fmt.Println
函数打印后,可以看到结果是“3 int
”,这也可以证明reflect.Value
表示的是变量的值,reflect.Type
表示的是变量的类型。
reflect.Value
reflect.Value
可以通过函数reflect.ValueOf
获得,下面将为你介绍它的结构和用法。
reflect.Value
结构体定义
在Go
语言中,reflect.Value
被定义为一个结构体,它的定义如下面的代码所示:
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
的方法。现在看看它有哪些常用方法,如下所示:
- 针对具体类型的系列方法
// 用户获取对应的值
Bool()
Bytes()
Complex()
Float()
Int()
String()
Uint()
CanAddr() // 是否可以使用Addr获取值的地址
CanSet() // 是否可以修改对应的值
// 用户修改对应的值
Set()
SetBool()
SetBytes()
SetComplex()
SetFloat()
SetInt()
SetString()
Elem() // 获取指针指向的值,一般用于修改对应的值
- 针对 struct 类型的系列方法
// Field 系列方法用于获取 struct 类型中的字段
Filed()
FiledByIndex()
FiledByName()
FiledByNameFunc()
Interface() // 获取对应的原始类型
IsNil() // 值是否为nil
IsZero() // 值是否是零值
Kind() // 获取对应的类型类别,比如 Array、Slice、Map 等
- 获取类型上的方法集
// 获取对应的方法
Method()
MethodByName()
NumField() //获取 struct 类型中字段的数量
NumMethod() // 获取类型上方法集的数量
Type() // 获取对应的 reflect.Type
看着比较多,其实就三类:
- 一类用于获取和修改对应的值;
- 一类与
struct
类型的字段有关,用于获取对应的字段; - 一类与类型上的方法集有关,用于获取对应的方法。
reflect.Type
reflect.Value
可以用于与值有关的操作,而如果是与变量类型本身有关的操作,比如要获取结构体对应的字段名称或方法,则最好使用reflect.Type
。
reflect.Type 接口定义
与reflect.Value
不同,reflect.Type
是一个接口,而不是一个结构体,所以也只能使用它的方法。
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
用于判断是否实现了接口 uAssignableTo
用于判断是否可以使用“=”,即赋值运算符
赋值给类型 uConvertibleTo
用于判断是否可以转换成类型 u,其实就是是否可以进行类型转换
Comparable
用于判断该类型是否可以使用关系运算符
进行比较
小技巧
你可以通过
FieldByName
方法获取指定的字段,也可以通过MethodByName
方法获取指定的方法,这在需要获取某个特定的字段或者方法而不是遍历时非常高效。
是否实现某接口?
通过 reflect.Type
中的Implements
可以判断是否实现了某接口。
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.Stringer
和io.Writer
接口, 运行结果:
是否实现了 fmt.Stringer: true
是否实现了 io.Writer: false
因为结构体person
只实现了fmt.Stringer
接口,没有实现io.Writer
接口,所以与验证的结果一致。
反射定律
反射是计算机语言中程序检视其自身结构的一种方法,它属于元编程的一种形式。反射灵活、强大,但也存在不安全因素。它可以绕过编译器的很多静态检查,如果过多使用便会造成混乱。为了帮助开发者更好地理解反射,Go语言的作者在博客上总结了反射的三大定律。
- 任何接口值
interface{}
都可以反射出反射对象,也就是reflect.Value
和reflect.Type
通过函数reflect.ValueOf
和reflect.TypeOf
获得。 - 反射对象也可以还原为
interface{}
变量,也就是第1条定律
的可逆性
,通过reflect.Value
结构体的Interface
方法获得。 - 要修改反射的对象,该值必须
可设置
,也就是可寻址
提示
任何类型的变量都可以转换为空接口
intferface{}
所以第1条定律中函数
reflect.ValueOf
和reflect.TypeOf
的参数就是interface{}
,表示可以把任何类型的变量转换为反射对象。在第2条定律中,
reflect.Value
结构体的Interface
方法返回的值也是interface{}
,表示可以把反射对象还原为对应的类型变量。
一旦你理解了这三大定律,就可以更好地理解和使用Go语言反射。
结束语
在反射中,reflect.Value
对应的是变量的值,如果你需要进行与变量的值有关的操作,应该优先使用reflect.Value
,比如获取变量的值、修改变量的值等。reflect.Type
对应的是变量的类型,如果你需要进行与变量的类型本身有关的操作,应该优先使用reflect.Type
,比如获取结构体内的字段、类型拥有的方法集等。
此外我要再次强调:反射虽然很强大,可以简化编程、减少重复代码,但是过度使用会让你的代码变得复杂混乱。所以除非非常必要,否则尽可能少地使用它们。
转载自:https://juejin.cn/post/7296111218720964643