饶全成:深度解密 Go 语言之反射(下)

阅读数:83 2019 年 9 月 17 日 18:42

饶全成:深度解密 Go 语言之反射(下)

本文篇幅过长,为方便阅读,分为上下两篇,此篇为下篇。Go 作为一门静态语言,相比 Python 等动态语言,在编写过程中灵活性会受到一定的限制。但是通过接口加反射实现了类似于动态语言的能力:可以在程序运行时动态地捕获甚至改变类型的信息和值。

反射相关函数的使用

代码样例

网络上各种博客文章里使用反射的样例代码非常多,读过这篇文章后,基本没有看不懂的,哈哈!不过,我这里还是举一个例子,并讲解一番:

复制代码
package main
import (
"reflect"
"fmt"
)
type Child struct {
Name string
Grade int
Handsome bool
}
type Adult struct {
ID string `qson:"Name"`
Occupation string
Handsome bool
}
// 如果输⼊参数 i 是 Slice,元素是结构体,有⼀个字段名为 `Handsome`,
// 并且有⼀个字段的 tag 或者字段名是 `Name` ,
// 如果该 `Name` 字段的值是 `qcrao`,
// 就把结构体中名为 `Handsome` 的字段值设置为 true。
func handsome(i interface{}) {
// 获取 i 的反射变量 Value
v := reflect.ValueOf(i)
// 确定 v 是⼀个 Slice
if v.Kind() != reflect.Slice {
return
}
// 确定 v 是的元素为结构体
if e := v.Type().Elem(); e.Kind() != reflect.Struct {
return
}
// 确定结构体的字段名含有 "ID" 或者 json tag 标签为 `name`
// 确定结构体的字段名 "Handsome"
st := v.Type().Elem()
// 寻找字段名为 Name 或者 tag 的值为 Name 的字段
foundName := false
for i := 0; i < st.NumField(); i++ {
f := st.Field(i)
tag := f.Tag.Get("qson")
if (tag == "Name" || f.Name == "Name") && f.Type.Kind() == reflect.String {
foundName = true
break
}
}
if !foundName {
return
}
if niceField, foundHandsome := st.FieldByName("Handsome"); foundHandsome == false || niceField.Type.Kind() != reflect.Boo
return
}
// 设置名字为 "qcrao" 的对象的 "Handsome" 字段为 true
for i := 0; i < v.Len(); i++ {
e := v.Index(i)
handsome := e.FieldByName("Handsome")
// 寻找字段名为 Name 或者 tag 的值为 Name 的字段
var name reflect.Value
for j := 0; j < st.NumField(); j++ {
f := st.Field(j)
tag := f.Tag.Get("qson")
if tag == "Name" || f.Name == "Name" {
name = v.Index(i).Field(j)
}
}
if name.String() == "qcrao" {
handsome.SetBool(true)
}
}
}
func main() {
children := []Child{
{Name: "Ava", Grade: 3, Handsome: true},
{Name: "qcrao", Grade: 6, Handsome: false},
}
adults := []Adult{
{ID: "Steve", Occupation: "Clerk", Handsome: true},
{ID: "qcrao", Occupation: "Go Programmer", Handsome: false},
}
fmt.Printf("adults before handsome: %v\n", adults)
handsome(adults)
fmt.Printf("adults after handsome: %v\n", adults)
fmt.Println("-------------")
fmt.Printf("children before handsome: %v\n", children)
handsome(children)
fmt.Printf("children after handsome: %v\n", children)
}

代码运行结果:

复制代码
adults before handsome: [{Steve Clerk true} {qcrao Go Programmer false}]
adults after handsome: [{Steve Clerk true} {qcrao Go Programmer true}]
-------------
children before handsome: [{Ava 3 true} {qcrao 6 false}]
children after handsome: [{Ava 3 true} {qcrao 6 true}]

代码主要做的事情是:找出传入的参数为 Slice,并且 Slice 的元素为结构体,如果其中有一个字段名是 Name 或者是 标签名称为 Name,并且还有一个字段名是 Handsome 的情形。如果找到,并且字段名称为 Name 的实际值是 qcrao 的话,就把另一个字段 Handsome 的值置为 true。

程序并不关心传入的结构体到底是什么,只要它的字段名包含 Name 和 Handsome,都是 handsome 函数要工作的对象。

注意一点, Adult 结构体的标签 qson:“Name”,中间是没有空格的,否则 Tag.Get(“qson”) 识别不出来。

未导出成员

利用反射机制,对于结构体中未导出成员,可以读取,但不能修改其值。

注意,正常情况下,代码是不能读取结构体未导出成员的,但通过反射可以越过这层限制。另外,通过反射,结构体中可以被修改的成员只有是导出成员,也就是字段名的首字母是大写的。

一个可取地址的 reflect.Value 变量会记录一个结构体成员是否是未导出成员,如果是的话则拒绝修改操作。 CanAddr 不能说明一个变量是否可以被修改。 CanSet 则可以检查对应的 reflect.Value 是否可取地址并可被修改。

复制代码
package main
import (
"reflect"
"fmt"
)
type Child struct {
Name string
handsome bool
}
func main() {
qcrao := Child{Name: "qcrao", handsome: true}
v := reflect.ValueOf(&qcrao)
f := v.Elem().FieldByName("Name")
fmt.Println(f.String())
f.SetString("stefno")
fmt.Println(f.String())
f = v.Elem().FieldByName("handsome")
// 这⼀句会导致 panic,因为 handsome 字段未导出
//f.SetBool(true)
fmt.Println(f.Bool())
}

执行结果:

复制代码
qcrao
stefno
true

上面的例子中,handsome 字段未导出,可以读取,但不能调用相关 set 方法,否则会 panic。反射用起来一定要小心,调用类型不匹配的方法,会导致各种 panic。

反射的实际应用

反射的实际应用非常广:IDE 中的代码自动补全功能、对象序列化(json 函数库)、fmt 相关函数的实现、ORM(全称是:Object Relational Mapping,对象关系映射)……

这里举 2 个例子:json 序列化和 DeepEqual 函数。

json 序列化

开发过 web 服务的同学,一定用过 json 数据格式。 json 是一种独立于语言的数据格式。最早用于浏览器和服务器之间的实时无状态的数据交换,并由此发展起来。

Go 语言中,主要提供 2 个函数用于序列化和反序列化:

复制代码
func Marshal(v interface{}) ([]byte, error)
func Unmarshal(data []byte, v interface{}) error

两个函数的参数都包含 interface,具体实现的时候,都会用到反射相关的特性。

对于序列化和反序列化函数,均需要知道参数的所有字段,包括字段类型和值,再调用相关的 get 函数或者 set 函数进行实际的操作。

DeepEqual 的作用及原理

在测试函数中,经常会需要这样的函数:判断两个变量的实际内容完全一致。

例如:如何判断两个 slice 所有的元素完全相同;如何判断两个 map 的 key 和 value 完全相同等等。

上述问题,可以通过 DeepEqual 函数实现。

复制代码
func DeepEqual(x, y interface{}) bool

DeepEqual 函数的参数是两个 interface,实际上也就是可以输入任意类型,输出 true 或者 flase 表示输入的两个变量是否是“深度”相等。

先明白一点,如果是不同的类型,即使是底层类型相同,相应的值也相同,那么两者也不是“深度”相等。

复制代码
type MyInt int
type YourInt int
func main() {
m := MyInt(1)
y := YourInt(1)
fmt.Println(reflect.DeepEqual(m, y)) // false
}

上面的代码中,m, y 底层都是 int,而且值都是 1,但是两者静态类型不同,前者是 MyInt,后者是 YourInt,因此两者不是“深度”相等。

在源码里,有对 DeepEqual 函数的非常清楚地注释,列举了不同类型,DeepEqual 的比较情形,这里做一个总结:

类型

深度相等情形

饶全成:深度解密 Go 语言之反射(下)

一般情况下,DeepEqual 的实现只需要递归地调用 == 就可以比较两个变量是否是真的“深度”相等。

但是,有一些异常情况:比如 func 类型是不可比较的类型,只有在两个 func 类型都是 nil 的情况下,才是“深度”相等;float 类型,由于精度的原因,也是不能使用 == 比较的;包含 func 类型或者 float 类型的 struct, interface, array 等。

对于指针而言,当两个值相等的指针就是“深度”相等,因为两者指向的内容是相等的,即使两者指向的是 func 类型或者 float 类型,这种情况下不关心指针所指向的内容。

同样,对于指向相同 slice, map 的两个变量也是“深度”相等的,不关心 slice, map 具体的内容。

对于“有环”的类型,比如循环链表,比较两者是否“深度”相等的过程中,需要对已比较的内容作一个标记,一旦发现两个指针之前比较过,立即停止比较,并判定二者是深度相等的。这样做的原因是,及时停止比较,避免陷入无限循环。

来看源码:

复制代码
func DeepEqual(x, y interface{}) bool {
if x == nil || y == nil {
return x == y
}
v1 := ValueOf(x)
v2 := ValueOf(y)
if v1.Type() != v2.Type() {
return false
}
return deepValueEqual(v1, v2, make(map[visit]bool), 0)
}

首先查看两者是否有一个是 nil 的情况,这种情况下,只有两者都是 nil,函数才会返回 true。

接着,使用反射,获取 x,y 的反射对象,并且立即比较两者的类型,根据前面的内容,这里实际上是动态类型,如果类型不同,直接返回 false。

最后,最核心的内容在子函数 deepValueEqual 中。

代码比较长,思路却比较简单清晰:核心是一个 switch 语句,识别输入参数的不同类型,分别递归调用 deepValueEqual 函数,一直递归到最基本的数据类型,比较 int,string 等可以直接得出 true 或者 false,再一层层地返回,最终得到“深度”相等的比较结果。

实际上,各种类型的比较套路比较相似,这里就直接节选一个稍微复杂一点的 map 类型的比较:

复制代码
// deepValueEqual 函数
// ……
case Map:
if v1.IsNil() != v2.IsNil() {
return false
}
if v1.Len() != v2.Len() {
return false
}
if v1.Pointer() == v2.Pointer() {
return true
}
for _, k := range v1.MapKeys() {
val1 := v1.MapIndex(k)
val2 := v2.MapIndex(k)
if !val1.IsValid() || !val2.IsValid() || !deepValueEqual(v1.MapIndex(k), v2.MapIndex(k), visited, depth+1) {
return false
}
}
return true
// ……

和前文总结的表格里,比较 map 是否相等的思路比较一致,也不需要多说什么。说明一点, visited 是一个 map,记录递归过程中,比较过的“对”:

复制代码
type visit struct {
a1 unsafe.Pointer
a2 unsafe.Pointer
typ Type
}
map[visit]bool

比较过程中,一旦发现比较的“对”,已经在 map 里出现过的话,直接判定“深度”比较结果的是 true。

总结

Go 作为一门静态语言,相比 Python 等动态语言,在编写过程中灵活性会受到一定的限制。但是通过接口加反射实现了类似于动态语言的能力:可以在程序运行时动态地捕获甚至改变类型的信息和值。

Go 语言的反射实现的基础是类型,或者说是 interface,当我们使用反射特性时,实际上用到的就是存储在 interface 变量中的和类型相关的信息,也就是常说的 <type,value> 对。

只有 interface 才有反射的说法。

反射在 reflect 包中实现,涉及到两个相关函数:

复制代码
func TypeOf ( i interface{} ) Type
func ValueOf ( i interface{} ) Value

Type 是一个接口,定义了很多相关方法,用于获取类型信息。Value 则持有类型的具体值。Type、Value、Interface 三者间通过函数 TypeOf,ValueOf,Interface 进行相互转换。

最后温习一下反射三大定律:

  1. Reflection goes from interface value to reflection object.

  2. Reflection goes from reflection object to interface value.

  3. To modify a reflection object, the value must be settable.

翻译一下:

  1. 反射将接口变量转换成反射对象 Type 和 Value;

  2. 反射可以通过反射对象 Value 还原成原先的接口变量;

  3. 反射可以用来修改一个变量的值,前提是这个值可以被修改。

本文转载自公众号滴滴技术(ID:didi_tech)。

原文链接:

https://mp.weixin.qq.com/s/HJMKwWgHqPIlD5nra99ZTw

评论

发布