从零学习 Go 语言(36):图解 Go 语言的反射三定律,so easy~

hello_wbm · · 737 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

![](http://image.iswbm.com/20200607145423.png) 在线博客:http://golang.iswbm.com/ Github:https://github.com/iswbm/GolangCodingTime --- 当我在使用 Python 的时候,我甚至可以做到不需要知道什么是内省,什么是反射,就可以立即使用内省去做一些事情。 而在学习 Go 语言后,反射在我这却变成了一个难点,一直感觉这个 **反射对象** 的概念异常的抽象。 这篇文章还是会跟上篇文章一样,尽量使用图解来解释一些抽象的概念,如果是我理解有误,还希望你在文章尾部给我留言指正,谢谢。 关于反射的内容,我分为了好几篇,这一篇是入门篇,会从经典的反射三大定律入手,写一些 demo 代码,告诉你反射的基本内容。 ## 1. 真实世界与反射世界 在本篇文章里,为了区分反射前后的变量值类型,我将反射前环境称为 **真实世界**,而将反射后的环境称为 **反射世界**。这种比喻不严谨,但是对于我理解是有帮助的,也希望对你有用。 在反射的世界里,我们拥有了获取一个对象的类型,属性及方法的能力。 ![](http://image.iswbm.com/20200614174556.png) ## 2. 两种类型:Type 和 Value 在 Go 反射的世界里,有两种类型非常重要,是整个反射的核心,在学习 reflect 包的使用时,先得学习下这两种类型: 1. reflect.Type 2. reflect.Value 它们分别对应着真实世界里的 type 和 value,只不过在反射对象里,它们拥有更多的内容。 从源码上来看,reflect.Type 是以一个接口的形式存在的 ```go type Type interface { Align() int FieldAlign() int Method(int) Method MethodByName(string) (Method, bool) NumMethod() int Name() string PkgPath() string Size() uintptr String() string Kind() Kind Implements(u Type) bool AssignableTo(u Type) bool ConvertibleTo(u Type) bool Comparable() bool Bits() int ChanDir() ChanDir IsVariadic() bool Elem() Type Field(i int) StructField FieldByIndex(index []int) StructField FieldByName(name string) (StructField, bool) FieldByNameFunc(match func(string) bool) (StructField, bool) In(i int) Type Key() Type Len() int NumField() int NumIn() int NumOut() int Out(i int) Type common() *rtype uncommon() *uncommonType } ``` 而 reflect.Value 是以一个结构体的形式存在, ```go type Value struct { typ *rtype ptr unsafe.Pointer flag } ``` 同时它接收了很多的方法(见下表),这里出于篇幅的限制这里也没办法一一介绍。 ```go Addr Bool Bytes runes CanAddr CanSet Call CallSlice call Cap Close Complex Elem Field FieldByIndex FieldByName FieldByNameFunc Float Index Int CanInterface Interface InterfaceData IsNil IsValid IsZero Kind Len MapIndex MapKeys MapRange Method NumMethod MethodByName NumField OverflowComplex OverflowFloat OverflowInt OverflowUint Pointer Recv recv Send send Set SetBool SetBytes setRunes SetComplex SetFloat SetInt SetLen SetCap SetMapIndex SetUint SetPointer SetString Slice Slice3 String TryRecv TrySend Type Uint UnsafeAddr assignTo Convert ``` 通过上一节的内容(),我们知道了一个接口变量,实际上都是由一 pair 对(type 和 data)组合而成,pair 对中记录着实际变量的值和类型。也就是说在真实世界里,type 和 value 是合并在一起组成 接口变量的。 而在反射的世界里,type 和 data 却是分开的,他们分别由 reflect.Type 和 reflect.Value 来表现。 ## 3. 解读反射的三大定律 Go 语言里有个反射三定律,是你在学习反射时,很重要的参考: 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. 反射可以将接口类型变量 转换为“反射类型对象”; 2. 反射可以将 “反射类型对象”转换为 接口类型变量; 3. 如果要修改 “反射类型对象” 其类型必须是 可写的; ### 第一定律 >Reflection goes from interface value to reflection object. 为了实现从接口变量到反射对象的转换,需要提到 reflect 包里很重要的两个方法: 1. reflect.TypeOf(i) :获得接口值的类型 2. reflect.ValueOf(i):获得接口值的值 这两个方法返回的对象,我们称之为反射对象:Type object 和 Value object。 ![golang reflection](http://image.iswbm.com/image-20200614175219320.png) 举个例子,看下这两个方法是如何使用的? ```go package main import ( "fmt" "reflect" ) func main() { var age interface{} = 25 fmt.Printf("原始接口变量的类型为 %T,值为 %v \n", age, age) t := reflect.TypeOf(age) v := reflect.ValueOf(age) // 从接口变量到反射对象 fmt.Printf("从接口变量到反射对象:Type对象的类型为 %T \n", t) fmt.Printf("从接口变量到反射对象:Value对象的类型为 %T \n", v) } ``` 输出如下 ```go 原始接口变量的类型为 int,值为 25 从接口变量到反射对象:Type对象的类型为 *reflect.rtype 从接口变量到反射对象:Value对象的类型为 reflect.Value ``` 如此我们完成了从接口类型变量到反射对象的转换。 等等,上面我们定义的 age 不是 int 类型的吗?第一法则里怎么会说是接口类型的呢? 关于这点,其实在上一节([关于接口的三个 『潜规则』]())已经提到过了,由于 TypeOf 和 ValueOf 两个函数接收的是 interface{} 空接口类型,而 Go 语言函数都是值传递,因此Go语言会将我们的类型隐式地转换成接口类型。 ```go // TypeOf returns the reflection Type of the value in the interface{}.TypeOf returns nil. func TypeOf(i interface{}) Type // ValueOf returns a new Value initialized to the concrete value stored in the interface i. ValueOf(nil) returns the zero Value. func ValueOf(i interface{}) Value ``` ### 第二定律 >Reflection goes from reflection object to interface value. 和第一定律刚好相反,第二定律描述的是,从反射对象到接口变量的转换。 ![golang reflection](http://image.iswbm.com/image-20200614175325721.png) 通过源码可知, reflect.Value 的结构体会接收 `Interface` 方法,返回了一个 `interface{}` 类型的变量(**注意:只有 Value 才能逆向转换,而 Type 则不行,这也很容易理解,如果 Type 能逆向,那么逆向成什么呢?**) ```go // Interface returns v's current value as an interface{}. // It is equivalent to: // var i interface{} = (v's underlying value) // It panics if the Value was obtained by accessing // unexported struct fields. func (v Value) Interface() (i interface{}) { return valueInterface(v, true) } ``` 这个函数就是我们用来实现将反射对象转换成接口变量的一个桥梁。 例子如下 ```go package main import ( "fmt" "reflect" ) func main() { var age interface{} = 25 fmt.Printf("原始接口变量的类型为 %T,值为 %v \n", age, age) t := reflect.TypeOf(age) v := reflect.ValueOf(age) // 从接口变量到反射对象 fmt.Printf("从接口变量到反射对象:Type对象的类型为 %T \n", t) fmt.Printf("从接口变量到反射对象:Value对象的类型为 %T \n", v) // 从反射对象到接口变量 i := v.Interface() fmt.Printf("从反射对象到接口变量:新对象的类型为 %T 值为 %v \n", i, i) } ``` 输出如下 ```go 原始接口变量的类型为 int,值为 25 从接口变量到反射对象:Type对象的类型为 *reflect.rtype 从接口变量到反射对象:Value对象的类型为 reflect.Value 从反射对象到接口变量:新对象的类型为 int 值为 25 ``` 当然了,最后转换后的对象,静态类型为 `interface{}` ,如果要转成最初的原始类型,需要再类型断言转换一下,关于这点,我已经在上一节里讲解过了,你可以点此前往复习:()。 ```go i := v.Interface().(int) ``` 至此,我们已经学习了反射的两大定律,对这两个定律的理解,我画了一张图,你可以用下面这张图来加强理解,方便记忆。 ![](http://image.iswbm.com/image-20200614194727218.png) ### 第三定律 >To modify a reflection object, the value must be settable. 反射世界是真实世界的一个『映射』,是我的一个描述,但这并不严格,因为并不是你在反射世界里所做的事情都会还原到真实世界里。 第三定律引出了一个 `settable` (可设置性,或可写性)的概念。 其实早在以前的文章中,我们就一直在说,Go 语言里的函数都是值传递,只要你传递的不是变量的指针,你在函数内部对变量的修改是不会影响到原始的变量的。 回到反射上来,当你使用 reflect.Typeof 和 reflect.Valueof 的时候,如果传递的不是接口变量的指针,反射世界里的变量值始终将只是真实世界里的一个拷贝,你对该反射对象进行修改,并不能反映到真实世界里。 因此在反射的规则里 - 不是接收变量指针创建的反射对象,是不具备『**可写性**』的 - 是否具备『**可写性**』,可使用 `CanSet()` 来获取得知 - 对不具备『**可写性**』的对象进行修改,是没有意义的,也认为是不合法的,因此会报错。 ```go package main import ( "fmt" "reflect" ) func main() { var name string = "Go编程时光" v := reflect.ValueOf(name) fmt.Println("可写性为:", v.CanSet()) } ``` 输出如下 ```go 可写性为: false ``` 要让反射对象具备可写性,需要注意两点 1. 创建反射对象时传入变量的指针 2. 使用 `Elem() `函数返回指针指向的数据 完整代码如下 ```go package main import ( "fmt" "reflect" ) func main() { var name string = "Go编程时光" v1 := reflect.ValueOf(&name) fmt.Println("v1 可写性为:", v1.CanSet()) v2 := v1.Elem() fmt.Println("v2 可写性为:", v2.CanSet()) } ``` 输出如下 ```go v1 可写性为: false v2 可写性为: true ``` 知道了如何使反射的世界里的对象具有可写性后,接下来是时候了解一下如何对修改更新它。 反射对象,都会有如下几个以 `Set` 单词开头的方法 ![](http://image.iswbm.com/image-20200614161613417.png) 这些方法就是我们修改值的入口。 来举个例子 ```go package main import ( "fmt" "reflect" ) func main() { var name string = "Go编程时光" fmt.Println("真实世界里 name 的原始值为:", name) v1 := reflect.ValueOf(&name) v2 := v1.Elem() v2.SetString("Python编程时光") fmt.Println("通过反射对象进行更新后,真实世界里 name 变为:", name) } ``` 输出如下 ```go 真实世界里 name 的原始值为: Go编程时光 通过反射对象进行更新后,真实世界里 name 变为: Python编程时光 ``` 参考文章 - [思否:Go 语言反射三定律](https://segmentfault.com/a/1190000006190038) ## 系列导读 --- [从零学习 Go 语言(01):一文搞定开发环境的搭建](https://studygolang.com/articles/27365) [从零学习 Go 语言(02):学习五种变量创建的方法](https://studygolang.com/articles/27432) [从零学习 Go 语言(03):数据类型之整型与浮点型](https://studygolang.com/articles/27440) [从零学习 Go 语言(04):byte、rune与字符串](https://studygolang.com/articles/27463) [从零学习 Go 语言(05):数据类型之数组与切片](https://studygolang.com/articles/27508) [从零学习 Go 语言(06):数据类型之字典与布尔类型](https://studygolang.com/articles/27563) [从零学习 Go 语言(07):数据类型之指针](https://studygolang.com/articles/27585) [从零学习 Go 语言(08):流程控制之if-else](https://studygolang.com/articles/27613) [从零学习 Go 语言(09):流程控制之switch-case](https://studygolang.com/articles/27660) [从零学习 Go 语言(10):流程控制之for 循环](https://studygolang.com/articles/28120) [从零学习 Go 语言(11):goto 无条件跳转](https://studygolang.com/articles/28472) [从零学习 Go 语言(12):流程控制之defer 延迟语句](https://studygolang.com/articles/28515) [从零学习 Go 语言(13):异常机制 panic 和 recover](https://studygolang.com/articles/28519) [从零学习 Go 语言(14):Go 语言中的类型断言是什么?](https://studygolang.com/articles/29305) [从零学习 Go 语言(15):学习 Go 语言的结构体与继承](https://studygolang.com/articles/29306) [从零学习 Go 语言(17):Go 语言中的 make 和 new 有什么区别?](https://studygolang.com/articles/29315) [从零学习 Go 语言(18):Go 语言中的 语句块与作用域](https://studygolang.com/articles/29365) [从零学习 Go 语言(19):Go Modules 前世今生及入门使用](https://studygolang.com/articles/29371) [从零学习 Go 语言(20):关于包导入必学的 8 个知识点](https://studygolang.com/articles/29404) [从零学习 Go 语言(21):一文了解 Go语言中编码规范](https://studygolang.com/articles/29477) [从零学习 Go 语言(22):Go 语言中如何开源自己写的包给别人用?](https://studygolang.com/articles/29609) [从零学习 Go 语言(23):一篇文章搞懂 Go 语言的函数](https://studygolang.com/articles/29628) [从零学习 Go 语言(24):理解 Go 语言中的 goroutine](https://studygolang.com/articles/29641) [从零学习 Go 语言(25):详解信道/通道](https://studygolang.com/articles/29704) [从零学习 Go 语言(26):通道死锁经典错误案例详解](https://studygolang.com/articles/29756) [从零学习 Go 语言(27):学习 Go 协程中的 WaitGroup](https://studygolang.com/articles/29783) [从零学习 Go 语言(28):学习 Go 协程中的互斥锁和读写锁](https://studygolang.com/articles/29838) [从零学习 Go 语言(29):Go 语言中的 select 用法](https://studygolang.com/articles/29852) [从零学习 Go 语言(30):如何使用 GDB 调试 Go 程序?](https://studygolang.com/articles/29867) [从零学习 Go 语言(31):Go 语言里的空接口](https://studygolang.com/articles/29885) [从零学习 Go 语言(32):理解 Go 语言中的 Context](https://studygolang.com/articles/29900) [从零学习 Go 语言(33):如何手动实现一个协程池?](https://studygolang.com/articles/29943) [从零学习 Go 语言(34):关于接口的 "潜规则"](https://studygolang.com/articles/29948) [从零学习 Go 语言(35):图解 Go 语言 静态类型与动态类型](https://studygolang.com/articles/30011) --- ![](http://image.python-online.cn/20200321153457.png)

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

737 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传