最近两三年来,写 Go 写得比较多,最近又写了一些 Python 代码,都有点怀念在以前使用 Python 的时候了。写起 Python 来,有一些动态特性特别得爽,其中一个就是反射说来就来,而且还特别简单,例如我写一个 Update 的时候:

  1. for field in update_fields:
  2. if old_obj.has_attr(field):
  3. old_obj.field = new_obj.field

这里都不用考虑两个对象是不是同样的,只需要关注 update 的字段是不是存在就可以了。当然,反射这种东西,很多编程语言都有,例如 Java 和 Go,C/C++ 有没有就不清楚了,但是,就 Java 或者 Go 来说,用起来就别扭一些了,至于怎么别扭,这篇文章我就是聊聊 Go 中的反射。

反射有什么用

在开始聊反射之前,我们得先知道凭什么我要用反射,和不用反射有多大区别?如果你有看过我以前写过的这篇文章:Go 语言中的深拷贝 的话,那么你可能就多少能领会到一些反射的必要性了。如果你不想看,那也没关系,那么假设我们想要对一个结构体进行一个深拷贝,那么可能我们普通的代码会这么写:

  1. newStruct := StructType {
  2. FieldA: oldStruct.FieldA,
  3. FieldB: oldStruct.FieldB,
  4. ... ...
  5. }

这么一看就非常啰嗦,而且,这个也只能针对一种具体的类型转换成另外一种具体的类型,假设我们有两种类型:ObjectProtoObjectDoc/ObjectTable 的话,那么可能就需要有 2 x 2 = 4 个拷贝函数了,极其麻烦。基于此,如果想要实现一种通用的方式,那么通过反射就可以帮助到我们实现这个目标。

核心数据类型

要想使用 Go 的反射,那么就得依赖于 reflect 包,而在 reflect 包中,有两个数据类型非常重要,分别是 TypeValue

那么这两个类型分别是怎么创建的以及如何使用呢?一般来说,我们在进行动态判断的时候都是因为接收到一个 interface{} 类型的对象,如果不知道它的真实类型,那么操作起来就会非常麻烦,可能你会尝试这样的方式进行:

  1. func main() {
  2. var a interface{} = 1
  3. if _, ok := a.(string); ok {
  4. fmt.Printf("a is string")
  5. return
  6. }
  7. if _, ok := a.(float32); ok {
  8. fmt.Printf("a is float32")
  9. return
  10. }
  11. if _, ok := a.(int); ok {
  12. fmt.Printf("a is int")
  13. return
  14. }
  15. }

反射获取值

这个在知道数据类型范围的情况下还好操作,但是如果不知道的话,那么如果这个对象是个自定义的 Struct 怎么办?所以这个时候 Value 就派上用场了,在 reflect 中,对于数据类型,它定义了范围,称为 Kind 分别是:

我们就可以通过获取这个对象的 ValueKind,然后根据 Kind 进行下一步的处理,例如:

  1. func main() {
  2. var a interface{} = demoStruct{1, 3.14}
  3. switch reflect.ValueOf(a).Kind() {
  4. case reflect.Int:
  5. fmt.Printf("a is int, and value is: %d\n", reflect.ValueOf(a).Int())
  6. case reflect.Struct:
  7. v := reflect.ValueOf(a)
  8. for i := 0; i < v.NumField(); i++ {
  9. fmt.Printf("a is struct, and field: %d is: %s\n", i, v.Type().Field(i).Name)
  10. }
  11. // ... ...
  12. }
  13. }

这里为了节省空间就展示一下 Type 和 Value 的一个简单用法,从 Sturct 的判断中可以看到,Type 可以用于获取对象的名字,其实还可以获取得更多,例如 Tag 之类的,运行这段代码就可以看到结果是:

  1. [root@liqiang.io]# go run demo03.go
  2. a is struct, and field: 0 is: A
  3. a is struct, and field: 1 is: D

完全没问题,和预料中地一样运行。现在我们已经能够通过反射获取一个未知类型对象的值了,那么下一步就该看看如何设置未知对象的值了。

反射设置值

反射设置值其实也特别简单,就像获取值是用 Value 一样,设置值也是用 Value,例如:

  1. func main() {
  2. var a = demoStruct{1, 3.14}
  3. firstField := reflect.ValueOf(&a).Elem().Field(0)
  4. firstField.SetInt(100)
  5. fmt.Printf("after set value, a.A = %d", a.A)
  6. }

执行一边看看,应该是和我们预期的一致:

  1. [root@liqiang.io]# go run demo04.go
  2. after set value, a.A = 100

这里有几个地方需要注意一下的:

  1. 修改值需要关注对象是否可以寻址(我的理解为变量对应的是一个地址,并且可以直接从地址中解析出值),reflect 也提供了两个函数帮助我们加强代码的鲁棒性:
  1. func main() {
  2. type t struct {
  3. N int
  4. }
  5. var n = t{42}
  6. // N at start
  7. fmt.Println(n.N)
  8. // pointer to struct - addressable
  9. ps := reflect.ValueOf(&n)
  10. // struct
  11. s := ps.Elem()
  12. if s.Kind() == reflect.Struct {
  13. // exported field
  14. f := s.FieldByName("N")
  15. if f.IsValid() {
  16. // A Value can be changed only if it is
  17. // addressable and was not obtained by
  18. // the use of unexported struct fields.
  19. if f.CanSet() {
  20. // change value of N
  21. if f.Kind() == reflect.Int {
  22. x := int64(7)
  23. if !f.OverflowInt(x) {
  24. f.SetInt(x)
  25. }
  26. }
  27. }
  28. }
  29. }
  30. // N at end
  31. fmt.Println(n.N)
  32. }
  1. 设置值,不能直接用 Value 类型的变量,因为它是不可寻址的,得重新获取一下 Elem()

小结

通过这篇文章的大概介绍,应该对 Go 语言中的反射有了一些大概的了解。其实在平时使用的很多代码中,都是使用到了反射的动态特性。然而,反射虽好,但是也不要沉醉于此,毕竟,方便带来的负面效果必然是性能代价,这个需要多做权衡。

Ref