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

2019 年 9 月 17 日

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

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


反射相关函数的使用


代码样例


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


package mainimport (    "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 := falsefor 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" 字段为 truefor 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 mainimport (    "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())}
复制代码


执行结果:


qcraostefnotrue
复制代码


上面的例子中,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 inttype YourInt intfunc main() {m := MyInt(1)y := YourInt(1)fmt.Println(reflect.DeepEqual(m, y)) // false}
复制代码


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


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


类型


深度相等情形



一般情况下,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.Pointera2 unsafe.Pointertyp Type}map[visit]bool
复制代码


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


总结


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


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


只有 interface 才有反射的说法。


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


func TypeOf ( i interface{} ) Typefunc 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


2019 年 9 月 17 日 18:42263

评论

发布
暂无评论
发现更多内容

谁能阻止世纪互联星光?2020年第11期北京机房网络质量评测数据排行榜发布

BonreeAPM

跨架构编程不再难,英特尔机器编程工具迎来重磅更新

intel001

架构师训练营第 11 周课后练习

叶纪想

极客大学架构师训练营

【技术分享】浅谈RTC及Agora RTC SDK集成介绍

邵帅

我理解的信息化、数字化、数智化

boshi

数字化 数据智能 信息化

公安情报指挥一体化合成作战系统开发解决方案

WX13823153201

IntelliJ IDEA 2020.3正式发布,年度最后一个版本很讲武德

YourBatman

IDEA 新特性 2020.3

JVM调优不知道怎么回答,阿里总结四大模块,学不会就背过来

996小迁

Java 架构 面试 调优

Spock单元测试框架实战指南一Spock是什么?它和JUnit有什么区别?

Java老k

Java 单元测试 JUnit Mock spock

极客大学 - 架构师训练营 第十一周总结

9527

智慧公安扫码一键定位报警系统搭建

t13823115967

智慧公安 智慧公安扫码 一键定位报警系统搭建

英特尔推动集成光电的发展,用于数据中心

intel001

Linux平台中调试C/C++内存泄漏方法 (腾讯和MTK面试的时候问到的)

linux大本营

c++ Linux 后台开发 架构师

Spring 源码学习 05:BeanDefinition 概念及其实现

程序员小航

spring 源码 源码阅读 BeanDefinition

架构设计之路-1

Dnnn

架构师 代码规范

英特尔发布第二代Horse Ridge低温量子控制芯片

intel001

AOT慈善币系统软件开发|AOT慈善币APP开发

开發I852946OIIO

系统开发 现成系统

甲方日常 62

句子

工作 随笔杂谈 日常

1分钟解密:博睿大数据核心引擎Bonree Zeus六大优势

BonreeAPM

架构师训练营第 1 期 -week11

习习

微软最强 Python 自动化工具开源了!不用写一行代码!

星安果

Python 微软 自动化 自动化测试 playwright

javascript开发后端程序的神器nodejs

程序那些事

Java nodejs 后端开发 koa 程序那些事

《穿越数据的迷宫》笔记:第 3 章 DAMA的数据管理原则

方志

数据治理

重点人员管控系统开发,可视化数据分析搭建

t13823115967

重点人员管控系统开发 智慧公安 可视化数据分析搭建

英特尔神经拟态生态系统发展和研究的最新进展

intel001

关于binlog,这个参数能不能用?

Simon

MySQL Binlog

面试无忧:源码+实践,讲到MySQL调优的底层算法实现

996小迁

Java 架构 面试

《技术男征服美女HR》—Fiber、Coroutine和多线程那些事

太白上仙

Java 程序员 面试 后端 多线程

加码线下,新荣耀“破题”场景经济

脑极体

国产电子表格Luckysheet后台也开源了!支持在线协作,一键docker私有部署

奇异石榴果

Java Excel SpreadJS 表格控件

腾讯大牛整合Java+spring5系统学习架构,神乎其技

小Q

Java 学习 编程 面试 spring 5

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