Go总结(十七)| 反射(reflect)
Go语言反射特性
(reflect)
提供了运行时动态获取对象的类型和值以及动态创建对象的能力。反射可以帮助抽象和简化代码,提高开发效率,但通常降低运行效率。
Go语言中的反射
Go语言提供了一种机制在运行时更新和检查变量的值、调用变量的方法和变量支持的内在操作,但是在编译时并不知道这些变量的具体类型,这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。
很多语言都有反射特性,在ORM相关框架中用的最多;也有些现代高级语言不支持反射遭到吐槽,比如Flutter开发中用的Dart。Go语言中反射的用法有其特点,下面我们看看例子:
|
|
运行结果如下:
&{Name:Go-Server IP:10.10.10.11 URL:chende.com Timeout:}
环境变量中设置的三个配置项已经生效。之后无论结构体 Config
内部的字段发生任何改变,这部分代码无需任何修改即可完美的适配,出错概率也极大地降低。
反射的类型(Type)与种类(Kind)
在使用反射时,需要首先理解类型(Type)和种类(Kind)的区别。编程中,使用最多的是类型,但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind)。例如需要统一判断类型中的指针时,使用种类(Kind)信息就较为方便。
1) 反射种类(Kind)的定义
Go语言程序中的类型(Type)指的是系统原生数据类型,如 int、string、bool、float32 等类型,以及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称。例如使用 type A struct{} 定义结构体时,A 就是 struct{} 的类型。
种类(Kind)指的是对象归属的品种,在 reflect 包中有如下定义:
type Kind uint
const (
Invalid Kind = iota // 非法类型
Bool // 布尔型
Int // 有符号整型
Int8 // 有符号8位整型
Int16 // 有符号16位整型
Int32 // 有符号32位整型
Int64 // 有符号64位整型
Uint // 无符号整型
Uint8 // 无符号8位整型
Uint16 // 无符号16位整型
Uint32 // 无符号32位整型
Uint64 // 无符号64位整型
Uintptr // 指针
Float32 // 单精度浮点数
Float64 // 双精度浮点数
Complex64 // 64位复数类型
Complex128 // 128位复数类型
Array // 数组
Chan // 通道
Func // 函数
Interface // 接口
Map // 映射
Ptr // 指针
Slice // 切片
String // 字符串
Struct // 结构体
UnsafePointer // 底层指针
)
Map、Slice、Chan 属于引用类型,使用起来类似于指针,但是在种类常量定义中仍然属于独立的种类,不属于 Ptr。type A struct{} 定义的结构体属于 Struct 种类,*A 属于 Ptr。
2) 从类型对象中获取类型名称和种类
Go语言中的类型名称对应的反射获取方法是 reflect.Type 中的 Name() 方法,返回表示类型名称的字符串;类型归属的种类(Kind)使用的是 reflect.Type 中的 Kind() 方法,返回 reflect.Kind 类型的常量。
反射的性能
毫无疑问的是,反射会增加额外的代码指令,对性能肯定会产生影响的。具体影响有多大,看看分情况测试。
创建对象
|
|
运行结果如下go test -bench=.
:
> go test -bench=.
goos: windows
goarch: amd64
pkg: yufa/demo/creflect
BenchmarkNew 37554218 28.8 ns/op
BenchmarkReflectNew 28631622 41.8 ns/op
PASS
ok yufa/demo/creflect 2.488s
看出来,反射的处理方式明显慢不少。
修改字段的值
通过反射获取结构体的字段有两种方式,一种是 FieldByName
,另一种是 Field
(index)。前面的例子中,我们使用的是 FieldByName
。
|
|
>go test -bench=.
goos: windows
goarch: amd64
pkg: yufa/demo/creflect
BenchmarkSet 1000000000 0.229 ns/op
BenchmarkReflect_FieldSet 56716939 21.5 ns/op
BenchmarkReflect_FieldByNameSet 5559864 214 ns/op
PASS
ok yufa/demo/creflect 3.040s
这里使用反射给每个字段赋值,相比直接赋值,性能差了约 100 - 1000 倍。其中,FieldByName
的性能相比 Field
差10 倍。
FieldByName
和Field
为什么会有这10倍的性能差异呢?看看源代码就知道了:FieldByName
中使用 for 循环,逐个字段查找,字段名匹配时返回。也就是说,在反射的内部,字段是按顺序存储的,因此按照下标访问查询效率为 O(1),而按照 Name
访问,则需要遍历所有字段,查询效率为 O(N)。结构体所包含的字段(包括方法)越多,那么两者之间的效率差距则越大。
怎样提高性能
避免使用反射
使用反射赋值,效率非常低下,如果有替代方案,尽可能避免使用反射,特别是会被反复调用的热点代码。例如 RPC 协议中,需要对结构体进行序列化和反序列化,这个时候避免使用 Go 语言自带的 json
的 Marshal
和 Unmarshal
方法,因为标准库中的 json 序列化和反序列化是利用反射实现的。可选的替代方案有 easyjson,在大部分场景下,相比标准库,有 5 倍左右的性能提升。或者采用Gin框架中推荐使用的jsoniter ,据说比easyjson还要快一些!
缓存
在上面的例子中可以看到,FieldByName
相比于 Field
有一个数量级的性能劣化。那在实际的应用中,就要避免直接调用 FieldByName
。我们可以利用字典将 Name
和 Index
的映射缓存起来。避免每次反复查找,耗费大量的时间。
我们利用缓存,再看看刚才的测试用例:
|
|
测试结果如下,不是10倍的差距,而是2倍:
>go test -bench=.
goos: windows
goarch: amd64
pkg: yufa/demo/creflect
BenchmarkSet 1000000000 0.225 ns/op
BenchmarkReflect_FieldSet 54694621 21.0 ns/op
BenchmarkReflect_FieldByNameSet 5419586 227 ns/op
BenchmarkReflect_FieldByNameCacheSet 25519588 45.1 ns/op
PASS
ok yufa/demo/creflect 4.213s
Golang标准库中对json的解析代码就是采用这里介绍的算法(缓存属性索引值):
|
|
总结
Go 作为一门静态语言,相比javascript
等动态语言,在编写过程中灵活性会受到一定的限制。但是通过接口加反射实现了类似于动态语言的能力,可以在程序运行时动态地捕获甚至改变类型的信息和值。
Go 语言的反射实现的基础是类型,或者说是 interface,当我们使用反射特性时,实际上用到的就是存储在 interface 变量中的和类型相关的信息,也就是常说的 <type, value>
对;只有 interface 才有反射的说法。
反射在 reflect 包中实现,涉及到两个相关函数:
|
|
Type 是一个接口,定义了很多相关方法,用于获取类型信息。Value 则持有类型的具体值。Type、Value、Interface 三者间通过函数 TypeOf,ValueOf,Interface 进行相互转换。
温习一下反射三大定律:
- 反射将接口变量转换成反射对象 Type 和 Value;
- 反射可以通过反射对象 Value 还原成原先的接口变量;
- 反射可以用来修改一个变量的值,前提是这个值可以被修改。
参考:
http://c.biancheng.net/view/4407.html
https://studygolang.com/articles/2157
(完)
- 原文作者: 闪电侠
- 原文链接:https://chende.ren/2021/01/23121026-017-reflect.html
- 版权声明:本作品采用 开放的「署名 4.0 国际 (CC BY 4.0)」创作共享协议 进行许可