13 反射 reflect
[TOC]
反射 reflect
Go是静态语言,但是通过反射可以获得一定的动态特性。
Go在运行时通过方法名要动态调用方法或者动态解析结构体等都要通过反射,反射让静态语言具有一定的动态特性。
反射作为框架或者基础服务的一部分是必不可少的。
反射三定律
反射可以从接口值得到反射对象。
反射可以从反射对象获得接口值。
若要修改一个反射对象,则其值必须是可以修改的。
reflect 标准库
Go反射的基础是编译器和运行时把类型信息以合适的数据结构保存在可执行程序中。
Go反射建立在Go类型系统基础之上,和接口有紧密的关系。
reflect标准库提供了一套访问接口,所有反射操作都基于reflect标准库。
反射的两种基本类型
reflect.Value 类型 通过 reflect.ValueOf(i inteface{}) Value
reflect.Type 类型 通过 reflect.TypeOf(i inteface{}) Type
ValueOf()和TypeOf()的形参都是inteface{},内存存储了即将被反射的变量,因此反射和接口之间存在很强的联系。
实例的值 reflect.Value
reflect.Value 实例通过 reflect.ValueOf() 函数获得
reflect.Value 表示的是实例的值信息,它本身是个struct,并绑定一系列的方法操作实例。
reflect.Value 常用方法
NumField() int
获得结构体字段数量 int
NumMethod() int
获得实例方法数量 int
MethodByName(name string) Value
根据方法名称 name 获取方法 Value
Method(i int) Value
根据方法索引 i 获得方法 Value
Elem() Type
间接访问,在反射实例是指针情况下使用获得指针指向的具体数据,如果反射的实例不是指针进行Elem()后再操作数据会panic,如果反射的实例是指针不进行Elem()后再操作数据也会panic。
Type() Type
获取反射实例的Type。
...
更多方法,标准库文档 https://studygolang.com/pkgdoc reflect标准库
通过空接口类型断言获得具体的值
reflect.Value中的interface()方法以空接口的形式返回reflect.Value中的值,要获取空接口真实的值可以通过类型断言对接口进行转换
通过reflect.Value绑定的方法获得具体的值
法获得具体类型的基础数据类型方法String()、Bool()、Int()、UInt()、Float()...,获得具体类型的时候同一类别下会进行统一转换,如int8、int16、int32在调用Int()方法转为具体类型时得到的都是int64
转换的类型与实际类型不相符会出现panic
间接访问 Elem() 和Indirect(v Value) Value
反射的是指针,就需要使用Elem()方法获得指针或接口执行的数据再进行访问。如果不是指针就不需要使用Elem()后再进行访问否则会panic。
还有一个替代函数,reflect.Indirect(v Value) Value 函数,如果v是指针则返回指针指向的数据Value,如果v不是指针也不会出现panic依然返回v自身。
反射指针
numValPtr是num指针的反射实例,直接调用numValPtr.Int()获得值会触发panic,需要使用Elem()进行间接访问
,reflect.Indirect(v Value) Value 源码
通过Kind进行判断是否指针以确定是否要Elem(),使用该函数可避免编码时再进行冗余的判断
修改反射实例的值
修改反射的值有多个方法可以使用,但是只有反射的实例是指针时才能赋值,反射的实例是指针就需要通过 Elem() 间接访问才能访问到数据
通过 SetXXX() 方法修改值
CanSet() 方法可以判断是否可以修改值
实例的类型 reflect.Type
rtype是反射包中一个通用的描述类型公共信息的结构体。
rtype实现了reflect.Type接口,通过reflect.TypeOf()函数获得反射实例的rtype,或者通过reflect.ValueOf().Type()获得。
reflect.TypeOf(num) 和 reflect.ValueOf(num).Type() 是相同的
reflect.Type 常用方法
reflect.Type 的方法分所有类型通用的方法和不同基础类型专有的方法。
类型通用方法
Name() string
获取包含包名的类型名字 string
Kind() Kind
获取该类型的底层基础类型 Kind
NumMethod() int
获取方法数量 int
Method(int) Method
通过索引 int 获取方法 Method
MethodByName(string) (Method,bool)
通过方法名获取方法 Method
New(typ Type) Value
根据 Type 创建一个实例, Value 是反射的实例指针
不同基础类型专用方法
如果调用别的类型专用的方法会出现panic,一般使用Kind()确定类型后再调用专用方法。
Elem() Type
获取实例的类型,Array、Chan、Map、Ptr、Slice类型专用。
NumField() int
获取字段数量 int,结构体专用。
Field(i int) StructField
通过索引 i 获取结构体字段,结构体专用。
FieldByName(name string) (StructField,bool)
通过字段名 name 获取结构体字段,结构体专用。
...
更多方法,标准库文档 https://studygolang.com/pkgdoc reflect标准库
基础类型 Kind
Kind是一个整数枚举值,不同的值代表不同的类型。
Kind的类型是一个抽象的概念,是一个大的类别,比如所有结构体的类别是struct,函数的类别是func。类别是根据编译器、运行时构建类型的内部数据结构不同来划分的,不同类别其构建的最终内部数据结构不同。
类别Kind和类型Type的区别
Kind 描述的是一个大的类别,Type描述的是一个具体的类型
基础数据类型int,Kind和Type都是int,但如果是自定义类型则Type是自定义类型的名称,Kind是自定义类型的类型。
如自定义类型struct TestKind,具体类型Type就是TestKind,而类别Kind是struct,同样是自定义类型的Integer也是如此。
结构体反射
反射在大部分情况下都是对结构体的操作,包括获得结构体字段、字段Tag、调用结构体方法。
定义案例用结构体:
Animal 结构体定义一个字段Category string,绑定了一个Sleep()方法。
Cat 结构体嵌套了Animal结构体,定义 Name string 字段, 并且定义了一个私有的 age int 字段。绑定一个Work(h int) int方法。
反射获取结构体字段、字段Tag
反射中结构体字段信息在Type中,值在Value中,因此Type和Value都需要
字段信息通过Type获取
字段值通过Value获取
嵌套结构体赋值
嵌套结构体3个注意事项
赋值注意1:赋值需要注意必须反射指针,不是指针不能赋值。
赋值注意2:结构体字段必须是公开的(首字母大写)才能被反射赋值,否则panic。
赋值注意3:被嵌套结构体的字段也是不可直接读取,需要使用被嵌套结构体的变量来读取。
反射调用方法
动态调用方法使用 Call([]reflect.Value) []reflect.Value
方法的信息在Type中获取,但是方法名是获取不到的。
私有方法(首字母小写)反射获取不到也不能调用。
反射无法直接访问组合的方法。
调用方法也要使用指针调用,否则方法绑定的变量将是nil,反射调用方法时编译器无法优化调用传参时的方法绑定参数。
调用方法如果是通过方法编号调用,注意方法的编号是按照ASCII码表排序,序号从0开始。
反射调用组合的方法
反射是无法直接读取到被嵌套的结构体的方法的,需要拿到被嵌套结构体的变量才能访问被嵌套结构体的方法。
案例中 Animal和Cat结构体的陷阱
在正常调用中自定义类型的方法绑定的是指针还是实例在调用时都可以通过指针或实例方式调用,这是编译器优化的结果。
但是在反射中,编译器是无法做出优化的,因此Animal结构体的 (ani Animal) Sleep() 方法是实例绑定在反射中只能实例调用,而Cat结构体的 (cat *Cat) Work(h int) int 方法是指针绑定只能指针调用。
如果将(ani Animal) Sleep()改为 (ani *Animal) Sleep() 则 【反射调用组合的方法】中的代码会panic,因为Cat结构体嵌入的是Ani Animal而不是Ani *Animal。
为了避免跳入这样的陷阱,编码中应该尽可能都采取一种绑定方法、一种嵌入方式。
(ani *Animal) Sleep() 这样绑定 则 嵌入结构体都采用 Ani *Animal。
反射案例,根据Model生成一条sql插入语句
根据Model使用反射生成SQL增加一条数据语句
读取 Model 的 Tag.table,生成一条插入数据的sql语句
Last updated