概述
Go是一门具有良好反射支持的静态语言。Go中提供的反射功能带来了很多动态特性。这种机制可以让我们在程序运行的时候动态地检查和修改对象的类型和值,比如说它可以实现以下的一些功能:
- 动态创建对象
- 动态调用函数
- 动态修改对象
- 动态获取对象信息,如类型、值、字段等信息
- 动态序列化和反序列化
值得注意的是的,反射无法获取变量的变量名。
基本概念
开始使用reflect
之前,我们需要了解reflect包里面的两个基本类型:
reflect.Type
是Go中的类型(type)的表示
reflect.Value
是Go中对值(value)的反射接口
并且,相对应的,还有两个函数来获取这两个基本类型:
reflect.TypeOf(i any) Type
reflect.ValueOf(i any) Value
示例:使用TypeOf
1
2
3
4
5
6
7
8
9
10
11
|
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}
|
会输出:
示例:使用ValueOf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package main
import (
"fmt"
"reflect"
)
func main() {
x := 3.14
fmt.Println("x value: ", reflect.ValueOf(x))
// 调用一些常用的方法
fmt.Println("x.type", reflect.ValueOf(x).Type())
fmt.Println("x.kind == reflect.Float64: ", reflect.ValueOf(x).Kind() == reflect.Float64) // 返回x的underlying type
fmt.Println("value of x: ", reflect.ValueOf(x).Float())
}
|
程序运行结果如下:
1
2
3
4
|
x value: 3.14
x.type float64
x.kind == reflect.Float64: true
value of x: 3.14
|
反射的三条定律
The Laws of Reflection一文中介绍到了Go语言中使用到反射的三条定律,理解这三条定律可以让我们更好的使用反射相关的内容。
定律1
Reflection goes from interface value to reflection object.
在基本层面上,反射只是一种检查存储在接口(interface)变量中的类型(type)和值(value)的机制。
举个例子来说,我们下面调用ValueOf(x)
:
1
2
|
x := 3.14
reflect.ValueOf(x)
|
实质上是先将x
存储在一个空的interface{}
中,然后再当作参数传递给reflect.ValueOf
,之后reflect.ValueOf
再从interface{}
中恢复类型信息。
定律2
Reflection goes from reflection object to interface value.
也就是说给定一个reflect.Value
,我们可以使用Interface
方法恢复一个接口值(interface value);实际上,该方法将类型和值信息打包回接口并返回结果:
1
2
|
// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}
|
定律3
To modify a reflection object, the value must be settable.
示例:不可修改
1
2
3
|
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.
|
这种是因为x
不能修改,若直接去修改x
的值,则会报错。
我们可以通过CanSet
方法去判断Value
是否可以被修改。
示例:将不可修改的示例改成可修改
实质上是通过指针去进行操作。
1
2
3
4
5
6
7
8
9
|
var x float64 = 3.4
p := reflect.ValueOf(&x)
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())
v := p.Elem() // To get to what p points to, we call the Elem method of Value, which indirects through the pointer
fmt.Println("settability of v:", v.CanSet())
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
|
程序运行结果如下:
1
2
3
4
5
|
type of p: *float64
settability of p: false
settability of v: true
7.1
7.1
|
为什么需要反射
其实反射某种程度就是元编程(meta programming),在部分情况通过常规编程很难做到的场景,我们需要通过反射来快速的达到我们的目的。
举个例子来说,假设现在我有一个生产环境的数据表定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// GORM定义的数据表
type RedisBase struct {
Time time.Time `gorm:"unique"`
Ip1In int
Ip1Out int
Ip2In int
Ip2Out int
Ip3In int
Ip3Out int
Ip4In int
Ip4Out int
Ip5In int
Ip5Out int
Ip6In int
Ip6Out int
}
|
是的,这个表看上去就是那么的不合理,但是历史环境造成的问题,很难去通过改数据表来进行,但是若此时我们的数据格式是:
1
2
3
|
// 若存在数据,则第一个对应ip1以此类推
var inData = []int{}
var outData = []int{}
|
如果我们这里不能使用反射,则需要用下面的方式写数据表:
1
2
3
4
5
6
7
|
r = RedisBase{
Ip1In inData[0]
Ip1Out outData[0]
Ip1In inData[1]
Ip1Out outData[1]
...
}
|
显得异常的繁琐,而我们这要是通过反射的方式,可以变得更加灵活:
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
|
package main
import (
"fmt"
"reflect"
)
// GORM定义的数据表
type RedisBase struct {
Ip1In int
Ip1Out int
Ip2In int
Ip2Out int
Ip3In int
Ip3Out int
Ip4In int
Ip4Out int
Ip5In int
Ip5Out int
Ip6In int
Ip6Out int
}
func main() {
var inData = []int{1, 2, 3, 4, 5, 6}
var outData = []int{6, 5, 4, 3, 2, 1}
r := RedisBase{}
v := reflect.ValueOf(&r).Elem()
for i := 0; i < len(inData); i++ {
v.FieldByName(fmt.Sprintf("Ip%dIn", i+1)).SetInt(int64(inData[i]))
v.FieldByName(fmt.Sprintf("Ip%dOut", i+1)).SetInt(int64(outData[i]))
}
fmt.Println(r) // {1 6 2 5 3 4 4 3 5 2 6 1}
}
|
这样写代码就会简单很多,可以写出更优雅、可复用的代码。
使用
struct
相关
reflect可以与struct
进行交互,主要是访问struct
以及修改struct
里面的字段。
读取struct
示例:通过反射读取struct的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package main
import (
"fmt"
"reflect"
)
type T struct {
A int
B string
}
func main() {
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i, typeOfT.Field(i).Name, f.Type(), f.Interface())
}
}
|
运行结果如下:
1
2
|
0: A int = 23
1: B string = skidoo
|
修改struct
的值
修改的struct
的字段需要是大写的字段名,否则是无法被访问的。
示例:通过反射修改struct的值
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
|
package main
import (
"fmt"
"reflect"
)
type T struct {
A int
B string
}
func main() {
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i, typeOfT.Field(i).Name, f.Type(), f.Interface())
}
// 修改字段的值
s.Field(0).SetInt(77)
s.FieldByName("B").SetString("Sunset Strip")
fmt.Println("t is now: ", t)
}
|
运行结果如下所示:
1
2
3
|
0: A int = 23
1: B string = skidoo
t is now: {77 Sunset Strip}
|
获取标签值
我们可以通过反射来检视结构体字段的标签信息。 结构体字段标签的类型为reflect.StructTag
,它的方法Get
和Lookup
用来检视字段标签中的键值对。
示例:获取struct的标签值
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
|
package main
import (
"fmt"
"reflect"
)
type T struct {
A int `max:"99" min:"0" defalut:"0"`
B string `optional:"yes"`
}
func main() {
t := reflect.TypeOf(T{})
x:= t.Field(0).Tag
y:=t.Field(1).Tag
fmt.Println("Tag type: ", reflect.TypeOf(x))
// v的类型为string
v, ok := x.Lookup("max")
fmt.Println(len(v), ok) // 2 true
fmt.Println(x.Lookup("optional")) // false
fmt.Println(y.Lookup("optional")) // yes true
}
|
程序输出内容:
1
2
3
4
|
Tag type: reflect.StructTag
2 true
false
yes true
|
slice
相关
当类型为slice
时,我们可以用Len
方法获取长度,并且通过Index
方法来获取对应的值。
示例:遍历interface
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func test2(value interface{}){
switch reflect.TypeOf(value).Kind(){
case reflect.Slice:
r := reflect.ValueOf(value)
for i:=0; i<r.Len(); i++{
log.Println(r.Index(i).Interface())
}
default:
log.Fatal("类型不为slice")
}
}
func main(){
test2([]string{"a", "b"})
test2(1)
}
|
参考文献
- The Laws of Reflection
- Go 101 反射
- reflect包官方文档