Go 学习笔记(十二)- 反射

反射是可以更新未知类型变量值的方法。

为何需要反射?

有时候我们需要编写一个函数能够处理一类并不满足普通公共接口的类型的值,也可能是因为它们并没有确定的表示方式,或者是在我们设计该函数的时候还这些类型可能还不存在,各种情况都有可能。

例如 fmt.Fprintf 函数的参数,我们通过之前的知识,可以利用接口断言实现,但是数组,结构体之类的组合后是无穷无尽的,所以必须借助反射实现。

reflect.Type 和 reflect.Value

函数 reflect.TypeOf 接受任意的 interface{} 类型, 并返回对应动态类型的 reflect.Type:

1
2
3
t := reflect.TypeOf(3)  // a reflect.Type
fmt.Println(t.String()) // "int"
fmt.Println(t) // "int"

和 reflect.TypeOf 类似, reflect.ValueOf 返回的结果也是对于具体的类型, 但是 reflect.Value 也可以持有一个接口值.

1
2
3
4
v := reflect.ValueOf(3) // a reflect.Value
fmt.Println(v) // "3"
fmt.Printf("%v\n", v) // "3"
fmt.Println(v.String()) // NOTE: "<int Value>"

递归打印

如果遇到聚合数据类型,就需要采用递归来显示。

通过 reflect.Value 修改值

有一些reflect.Values是可取地址的;其它一些则不可以。考虑以下的声明语句:

1
2
3
4
5
x := 2                   // value   type    variable?
a := reflect.ValueOf(2) // 2 int no
b := reflect.ValueOf(x) // 2 int no
c := reflect.ValueOf(&x) // &x *int no
d := c.Elem() // 2 int yes (x)

我们可以通过调用reflect.Value的CanAddr方法来判断其是否可以被取地址:

1
2
3
4
fmt.Println(a.CanAddr()) // "false"
fmt.Println(b.CanAddr()) // "false"
fmt.Println(c.CanAddr()) // "false"
fmt.Println(d.CanAddr()) // "true"

获取结构体字段标识

reflect.TypeField 方法将返回一个 reflect.StructField,里面含有每个成员的名字、类型和可选的成员标签等信息。其中成员标签信息对应 reflect.StructTag 类型的字符串,并且提供了Get方法用于解析和根据特定key提取的子串。

类型方法枚举

使用reflect.Type来打印任意值的类型和枚举它的方法:

1
2
3
4
5
6
7
8
9
10
11
func Print(x interface{}) {
v := reflect.ValueOf(x)
t := v.Type()
fmt.Printf("type %s\n", t)

for i := 0; i < v.NumMethod(); i++ {
methType := v.Method(i).Type()
fmt.Printf("func (%s) %s%s\n", t, t.Method(i).Name,
strings.TrimPrefix(methType.String(), "func"))
}
}

几点忠告

反射是一个强大并富有表达力的工具,但是它应该被小心地使用,原因有三。
第一个原因是,基于反射的代码是比较脆弱的。
第二个原因是,即使对应类型提供了相同文档,但是反射的操作不能做静态类型检查,而且大量反射的代码通常难以理解。
第三个原因,基于反射的代码通常比正常的代码运行速度慢一到两个数量级。

小结

比较抽象,但是大致意思可以理解,就是通过 reflect 包操作任意类型变量。