最近两三年来,写 Go 写得比较多,最近又写了一些 Python 代码,都有点怀念在以前使用 Python 的时候了。写起 Python 来,有一些动态特性特别得爽,其中一个就是反射说来就来,而且还特别简单,例如我写一个 Update 的时候:
for field in update_fields:
if old_obj.has_attr(field):
old_obj.field = new_obj.field
这里都不用考虑两个对象是不是同样的,只需要关注 update 的字段是不是存在就可以了。当然,反射这种东西,很多编程语言都有,例如 Java 和 Go,C/C++ 有没有就不清楚了,但是,就 Java 或者 Go 来说,用起来就别扭一些了,至于怎么别扭,这篇文章我就是聊聊 Go 中的反射。
反射有什么用
在开始聊反射之前,我们得先知道凭什么我要用反射,和不用反射有多大区别?如果你有看过我以前写过的这篇文章:Go 语言中的深拷贝 的话,那么你可能就多少能领会到一些反射的必要性了。如果你不想看,那也没关系,那么假设我们想要对一个结构体进行一个深拷贝,那么可能我们普通的代码会这么写:
newStruct := StructType {
FieldA: oldStruct.FieldA,
FieldB: oldStruct.FieldB,
... ...
}
这么一看就非常啰嗦,而且,这个也只能针对一种具体的类型转换成另外一种具体的类型,假设我们有两种类型:ObjectProto
和 ObjectDoc
/ObjectTable
的话,那么可能就需要有 2 x 2 = 4 个拷贝函数了,极其麻烦。基于此,如果想要实现一种通用的方式,那么通过反射就可以帮助到我们实现这个目标。
核心数据类型
要想使用 Go 的反射,那么就得依赖于 reflect 包,而在 reflect 包中,有两个数据类型非常重要,分别是 Type
和 Value
,
reflect.Type
:表示的是 Go 语言中的一个类型,只有一个实例,那就是类型描述符reflect.Value
:包含任意类型得值
那么这两个类型分别是怎么创建的以及如何使用呢?一般来说,我们在进行动态判断的时候都是因为接收到一个 interface{}
类型的对象,如果不知道它的真实类型,那么操作起来就会非常麻烦,可能你会尝试这样的方式进行:
func main() {
var a interface{} = 1
if _, ok := a.(string); ok {
fmt.Printf("a is string")
return
}
if _, ok := a.(float32); ok {
fmt.Printf("a is float32")
return
}
if _, ok := a.(int); ok {
fmt.Printf("a is int")
return
}
}
反射获取值
这个在知道数据类型范围的情况下还好操作,但是如果不知道的话,那么如果这个对象是个自定义的 Struct 怎么办?所以这个时候 Value
就派上用场了,在 reflect
中,对于数据类型,它定义了范围,称为 Kind
分别是:
- 基础类型: Bool,String 等等
- 聚合类型:Array 和 Struct
- 引用类型:Chan,Func,Ptr,Slace 和 Map
- 借口类型:Interface
- Invalid:表示还没有任何值
我们就可以通过获取这个对象的 Value
的 Kind
,然后根据 Kind
进行下一步的处理,例如:
func main() {
var a interface{} = demoStruct{1, 3.14}
switch reflect.ValueOf(a).Kind() {
case reflect.Int:
fmt.Printf("a is int, and value is: %d\n", reflect.ValueOf(a).Int())
case reflect.Struct:
v := reflect.ValueOf(a)
for i := 0; i < v.NumField(); i++ {
fmt.Printf("a is struct, and field: %d is: %s\n", i, v.Type().Field(i).Name)
}
// ... ...
}
}
这里为了节省空间就展示一下 Type 和 Value 的一个简单用法,从 Sturct 的判断中可以看到,Type 可以用于获取对象的名字,其实还可以获取得更多,例如 Tag 之类的,运行这段代码就可以看到结果是:
[root@liqiang.io]# go run demo03.go
a is struct, and field: 0 is: A
a is struct, and field: 1 is: D
完全没问题,和预料中地一样运行。现在我们已经能够通过反射获取一个未知类型对象的值了,那么下一步就该看看如何设置未知对象的值了。
反射设置值
反射设置值其实也特别简单,就像获取值是用 Value
一样,设置值也是用 Value
,例如:
func main() {
var a = demoStruct{1, 3.14}
firstField := reflect.ValueOf(&a).Elem().Field(0)
firstField.SetInt(100)
fmt.Printf("after set value, a.A = %d", a.A)
}
执行一边看看,应该是和我们预期的一致:
[root@liqiang.io]# go run demo04.go
after set value, a.A = 100
这里有几个地方需要注意一下的:
- 修改值需要关注对象是否可以寻址(我的理解为变量对应的是一个地址,并且可以直接从地址中解析出值),reflect 也提供了两个函数帮助我们加强代码的鲁棒性:
func main() {
type t struct {
N int
}
var n = t{42}
// N at start
fmt.Println(n.N)
// pointer to struct - addressable
ps := reflect.ValueOf(&n)
// struct
s := ps.Elem()
if s.Kind() == reflect.Struct {
// exported field
f := s.FieldByName("N")
if f.IsValid() {
// A Value can be changed only if it is
// addressable and was not obtained by
// the use of unexported struct fields.
if f.CanSet() {
// change value of N
if f.Kind() == reflect.Int {
x := int64(7)
if !f.OverflowInt(x) {
f.SetInt(x)
}
}
}
}
}
// N at end
fmt.Println(n.N)
}
- 设置值,不能直接用
Value
类型的变量,因为它是不可寻址的,得重新获取一下Elem()
。
小结
通过这篇文章的大概介绍,应该对 Go 语言中的反射有了一些大概的了解。其实在平时使用的很多代码中,都是使用到了反射的动态特性。然而,反射虽好,但是也不要沉醉于此,毕竟,方便带来的负面效果必然是性能代价,这个需要多做权衡。