六游的博客小站
Golang中的反射与接口详解
发布于: 2019-09-26 更新于: 2019-09-26 阅读次数: 

反射

反射是许多强类型语言都会具有的功能,我们先看一下维基百科中对反射的定义

在计算机科学中,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。

反射带给我们了一种在高级语言中直接获取对象内存中的数据的途径,如果没有反射但我们却想获取对象的类型与内存信息,就只能使用汇编才能做到

为什么我们需要反射(反射的优点)

  • 使用反射可以让我们在编写程序时,避免一部分的硬代码,使代码具有灵活性,变通性
  • 使用反射给我们了在程序运行时修改源码结构(如方法,隐藏字段值,结构体定义等),可以给程序带来更大的可能性

反射有什么缺点?

  • 因为反射的语法与概念都比较抽象,所以使用了反射的代码会使人难以阅读,不利于合作与交流
  • 因为反射将部分信息检查工作从编译期推到了运行期,所以会影响代码执行的效率,滥用反射可能会很大程度降低程序的运行效率
  • 面向反射的编程需要较多的高级知识,学习成本高。并且写出错误的反射代码的代价也很高,通常会直接引起程序的panic

Golang中的接口

因为Golang中的反射主要依赖于Golang中的接口类型,所以在我们正式开始学习反射之前,要先了解一下Golang中的接口。我想接触过Golang的程序员应该都知道Golang的Duck Typing,即“一个东西像鸭子一样有嘴巴,有眼睛,会游泳,会嘎嘎叫,那么我们就可以把这个东西看成鸭子”。在Golang中一个类型只要实现了某个接口中所有的方法,那么该类型就可以看作是实现了这个接口,使用隐式的接口声明。

在Golang的源码中,与接口相关的结构体主要有eface与iface这两个,这两个结构体的区别是,eface主要用来保存空接口对象,iface则用来保存实现了其他非空接口的对象。为什么空接口与非空接口需要两个不同的结构体来保存呢?这是与Golang的类型系统有关的,隐式的接口类型声明决定了某个对象的接口类型是在运行时才能被确定的,这种可能随程序运行而改变的类型,被称作动态类型。这样Golang中的空接口因为没有实现任何接口,所以只拥有静态类型数据,而实现了其他接口的对象,则同时拥有静态类型与动态类型。下面贴出了iface,eface以及其中涉及到的一些结构体的源码,可以看到iface与eface中都有data字段,该字段保存了一个指向接口对象数据的指针。_type用来存储静态类型,itab做了一层包装,同时存储动态类型与静态类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
type iface struct {
tab *itab
data unsafe.Pointer
}

type eface struct {
_type *_type
data unsafe.Pointer
}

type itab struct {
inter *interfacetype
_type *_type
hash uint32 // 与type结构体中的hash一致,用于类型转换
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

type _type struct {
//类型大小相关
size uintptr
ptrdata uintptr
//类型的hash值
hash uint32
//与反射相关
tflag tflag
//与内存对齐相关
align uint8
fieldalign uint8
//类型的编号,标注类型是bool,slice,struct等等
kind uint8
alg *typeAlg
//以下字段存储与GC相关的数据
gcdata *byte
str nameOff
ptrToThis typeOff
}

反射的基本函数

在reflect包中定义了一个Type接口以及一个Value结构体,分别用来获取对象的类型信息,与获取修改对象的数据。同时也提供了两个基本函数分别来返回一个对象的Type接口与Value结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

//首先将传入的对象数据转换会eface类型(emptyInterface与eface一致),再拿到eface中的_type字段中的数据,从而获取到对象的类型信息
func TypeOf(i interface{}) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}

func ValueOf(i interface{}) Value {
if i == nil {
return Value{}
}

escapes(i)

return unpackEface(i)
}

接下来我们来看一下Type接口中拥有哪些方法,以及Value结构体中的字段以及实现的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
type Type interface {


//返回变量对齐后占用的字节数
Align() int

//struct中的字段对齐后占用的字节数
FieldAlign() int

//返回方法集中第i个方法
Method(int) Method

//通过方法名称获取方法
MethodByName(string) (Method, bool)

//返回类型方法集中的方法个数
NumMethod() int

//如果是已定义的类型,则返回包内类型名称,如果未定义则返回空字符串
Name() string

//返回包路径名,也就是我们需要import的那一串字符
PkgPath() string

//返回类型大小
Size() uintptr

//返回类型的字符形式
String() string

//返回类型的类型值
Kind() Kind

//判断该类型是否实现了类型u
Implements(u Type) bool

//判断该类型是否可以复制给u类型变量
AssignableTo(u Type) bool

//判断该类型是否可以转换为u类型
ConvertibleTo(u Type) bool

//判断该类型是否可以进行比较
Comparable() bool
//类型所占据的位数
Bits() int

//返回channel的方向,只能channel类型调用
ChanDir() ChanDir

//判断方法是否含有可变参数,只能方法类型调用
IsVariadic() bool

//返回子元素类型,只能map,slice,chan,array等类型调用
Elem() Type

//返回结构体第i个字段,只能结构体类型调用
Field(i int) StructField


FieldByIndex(index []int) StructField

//通过结构体字段名获取字段
FieldByName(name string) (StructField, bool)

//传入一个名称过滤函数,根据函数获取结构体字段
FieldByNameFunc(matchfunc(string)bool(StructField,bool))

//获取函数类型第i个参数的类型,只能函数类型调用
In(i int) Type

//获取Map key的类型,只能map类型调用
Key() Type

//获取array类型的长度
Len() int

//获取struc类型中的字段数量
NumField() int

//获取函数类型中参数的个数,只能函数类型调用
NumIn() int

//获取函数类型返回值个数,只能函数类型调用
// It panics if the type's Kind is not Func.
NumOut() int

//获取函数类型第i个返回值的类型,只能函数类型调用
Out(i int) Type

common() *rtype
uncommon() *uncommonType
}
//Value结构体以及Value结构体实现的方法
type Value struct {
//等同于eface中的_type字段
typ *rtype
//指向对象数据的指针
ptr unsafe.Pointer
flag
}

// 设置切片的 len 字段,如果类型不是切片,就会panic
func (v Value)SetLen(n int){}

// 设置切片的 cap 字段
func (v Value) SetCap(n int){}

// 设置字典的 kv
func (v Value) SetMapIndex(key, val Value){}

// 返回切片、字符串、数组的索引 i 处的值
func (v Value) Index(i int) Value{}

// 根据名称获取结构体的内部字段值
func (v Value) FieldByName(name string) Value{}

// 获取 int 类型的值
func (v Value) Int() int64{}

// 获取结构体字段(成员)数量
func (v Value) NumField() int{}

// 尝试向通道发送数据(不会阻塞)
func (v Value) TrySend(x reflect.Value) bool{}

// 通过参数列表 in 调用 v 值所代表的函数(或方法
func (v Value) Call(in[]Value) (r []Value) {}

// 调用变参长度可变的函数
func (v Value) CallSlice(in[]Value) []Value{}

反射代码用例

导出结构体中的未导出成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import(
"reflect"
"fmt"
)

type People struct{

Name string
age int
}

func main() {

liuyou := People{Name: "Liuyou", age: 19}

v := reflect.ValueOf(&liuyou)

//首先使用反射对已导出字段操作
f := v.Elem().FieldByName("Name")
fmt.Println(f.String())
f.SetString("yqz")
fmt.Println(f.String())
//接着获取结构体内未到处字段
f = v.Elem().FieldByName("age")
f.SetInt(20)
fmt.Println(f.Int())

}

使用反射实现简易的validate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
func Validate(value interface{}) (ok bool, msg string) {
//获取传入interface对象所对应的Value与Type
t := reflect.TypeOf(value)
v := reflect.ValueOf(value)
//这是基于结构体的验证器,判断如果传入的不是一个结构体就返回
if t.Kind() != reflect.Struct {
return false, "传入对象必须是一个结构体"
}
//遍历结构体中的所有字段,如果对应字段设置了我们所指定的tag标签(这里是validate),就将该字段的值与标签中用户所写下的规则匹配
for i := 0; i < v.NumField(); i++ {
tag := t.Field(i).Tag
pattern := tag.Get("validate")

if pattern == "" {
return
}
//多个规则使用逗号隔开
patternList := strings.Split(pattern, ",")
for _, pattern = range patternList {
//每匹配到一个特定的规则,就交给对应规则的处理函数
if strings.HasPrefix(pattern, "required") {
ok, msg = checkRequired(pattern, v.Field(i))
if !ok {
return
}
} else if strings.HasPrefix(pattern, "number") {
ok, msg = checkNumber(pattern, v.Field(i))
if !ok {
return
}
} else if strings.HasPrefix(pattern, "max") {
ok, msg = checkMax(pattern, v.Field(i))
if !ok {
return
}
} else if strings.HasPrefix(pattern, "min") {
ok, msg = checkMin(pattern, v.Field(i))
if !ok {
return
}
} else if strings.HasPrefix(pattern, "email") {
ok, msg = checkEmail(pattern, v.Field(i))
if !ok {
return
}
} else if strings.HasPrefix(pattern, "phone") {
ok, msg = checkPhone(pattern, v.Field(i))
if !ok {
return
}
}
}

}
return
}

这里只给出checkRequired(检查字段是否为空)的规则处理函数,其余函数的实现原理大致一致,就是对每个字段对应的Value进行正则匹配,检查等,判断该字段的值是否符合对应的规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func checkRequired(pattern string, value reflect.Value) (ok bool, msg string) {
//required规则的书写方式定义为required:msg,msg为判断出错的时候返回的错误信息,如果没有填写msg则按照默认的信息提示返回
ruleList := strings.Split(pattern, ":")

if value.IsValid() {
ok = true
return
}

if len(ruleList) >= 2 {
msg = ruleList[1]
} else {
msg = "某个必需的字段为空"
}
ok = false

return
}

结束

好了,关于Golang反射的机制我们今天就先学习到这里了。每个事物都有自己的优点也有自己的缺点,反射可以通过获取对象的动态类型或者接口对象的值来让我们的代码更加具有兼容性与变通性,但程序中过多的反射代码,不仅会拖慢程序的运行,而且会使代码难以令外人阅读。所以在你的代码中适当(注意是适当哦)的使用反射,来使你的代码更加优雅!

--- 本文结束 The End ---