深度解密Go语言之反射
目录
反射和 interface 息息相关,而 interface 是我们上一篇文章的内容。在开始正文前,和大家说点题外话。
上一篇关于 interface 的文章发出后,获得了很多的关注和阅读。比如,登上了 gocn 的每日新闻第一条:
可能是编辑者觉得这篇文章称不上“深度解密”,把标题给小小地改动了下,哈哈~~
在博客园登上了 48 小时阅读排行榜:
在开发者头条 app (类似于今日头条,不过内容都是技术相关的,还挺有意思的)上收获了 150 收藏,并被推荐到首页最显眼的 banner 位置,阅读量达到了 1w 多,只是不知道这个数字是否是真实的,有点难以相信。
很多同学在后台向我反映文章太长了,不利于阅读,建议拆分一下。我非常理解,读屏时代,大家需要快速地读完全文,拿到收益。而码农桃花源的文章都非常长,读者很难在短时间内读完,并且获得相应的收益。
首先非常感谢大家的建议!其实我的想法是这样的:大家都在说,现在是一个信息严重过载的时代,信息多得看不完,不免产生很多焦虑。但是,我想说,优质的信息真有那么多吗?在我看来,文章的水平都是参差不齐,很多毫无内容和价值,大家把时间浪费在这些信息上面是很不值得的。因为你读了这些文章,就没有了读其他好的文章的精力。
所以,码农桃花源想做一个优质信息源,提供优质的内容。每一篇文章都是有深度,有内容,有收获。一篇文章,我一般得花费 2 周左右,算是半月更,和那些日更的没法比。当然,只是在数量上没法比。而这个时代,最不缺的就是数量。
另外,文章长也算是我的一个特色。我完全可以拆分成上、中、下等等,但我希望一次性交付给我的读者所有有价值的内容。这样,你可以集中一个小时或是更长时间,精读完一篇文章。
闲话结束,今天要讲的内容是反射,进入正题。
什么是反射
直接看*上的定义:
在计算机科学中,反射是指计算机程序在运行时(run time)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。
那我就要问个问题了:不用反射就不能在运行时访问、检测和修改它本身的状态和行为吗?
问题的回答,其实要首先理解什么叫访问、检测和修改它本身状态或行为,它的本质是什么?
实际上,它的本质是程序在运行期探知对象的类型信息和内存结构,不用反射能行吗?可以的!使用汇编语言,直接和内层打交道,什么信息不能获取?但是,当编程迁移到高级语言上来之后,就不行了!就只能通过反射
来达到此项技能。
不同语言的反射模型不尽相同,有些语言还不支持反射。《go 语言圣经》中是这样定义反射的:
go 语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,但是在编译时并不知道这些变量的具体类型,这称为反射机制。
为什么要用反射
需要反射的 2 个常见场景:
- 有时你需要编写一个函数,但是并不知道传给你的参数类型是什么,可能是没约定好;也可能是传入的类型很多,这些类型并不能统一表示。这时反射就会用的上了。
- 有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定。这时就需要对函数和函数的参数进行反射,在运行期间动态地执行函数。
在讲反射的原理以及如何用之前,还是说几点不使用反射的理由:
- 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。
- go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。
- 反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。
反射是如何实现的
上一篇文章讲到了 interface
,它是 go 语言实现抽象的一个非常强大的工具。当向接口变量赋予一个实体类型的时候,接口会存储实体的类型信息,反射就是通过接口的类型信息实现的,反射建立在类型的基础上。
go 语言在 reflect 包里定义了各种类型,实现了反射的各种函数,通过它们可以在运行时检测类型的信息、改变类型的值。
types 和 interface
go 语言中,每个变量都有一个静态类型,在编译阶段就确定了的,比如 int, float64, []int
等等。注意,这个类型是声明时候的类型,不是底层数据类型。
go 官方博客里就举了一个例子:
type myint int var i int var j myint
尽管 i,j 的底层类型都是 int,但我们知道,他们是不同的静态类型,除非进行类型转换,否则,i 和 j 不能同时出现在等号两侧。j 的静态类型就是 myint
。
反射主要与 interface{} 类型相关。前面一篇关于 interface 相关的文章已经探讨过 interface 的底层结构,这里再来复习一下。
type iface struct { tab *itab data unsafe.pointer } type itab struct { inter *interfacetype _type *_type link *itab hash uint32 bad bool inhash bool unused [2]byte fun [1]uintptr }
其中 itab
由具体类型 _type
以及 interfacetype
组成。_type
表示具体类型,而 interfacetype
则表示具体类型实现的接口类型。
实际上,iface 描述的是非空接口,它包含方法;与之相对的是 eface
,描述的是空接口,不包含任何方法,go 语言里有的类型都 “实现了”
空接口。
type eface struct { _type *_type data unsafe.pointer }
相比 iface
,eface
就比较简单了。只维护了一个 _type
字段,表示空接口所承载的具体的实体类型。data
描述了具体的值。
还是用 go 官方关于反射的博客里的例子,当然,我会用图形来详细解释,结合两者来看会更清楚。顺便提一下,搞技术的不要害怕英文资料,要想成为技术专家,读英文原始资料是技术提高的一条必经之路。
先明确一点:接口变量可以存储任何实现了接口定义的所有方法的变量。
go 语言中最常见的就是 reader
和 writer
接口:
type reader interface { read(p []byte) (n int, err error) } type writer interface { write(p []byte) (n int, err error) }
接下来,就是接口之间的各种转换和赋值了:
var r io.reader tty, err := os.openfile("/users/qcrao/desktop/test", os.o_rdwr, 0) if err != nil { return nil, err } r = tty
首先声明 r
的类型是 io.reader
,注意,这是 r
的静态类型,此时它的动态类型为 nil
,并且它的动态值也是 nil
。
之后,r = tty
这一语句,将 r
的动态类型变成 *os.file
,动态值则变成非空,表示打开的文件对象。这时,r 可以用<value, type>
对来表示为: <tty, *os.file>
。
注意看上图,此时虽然 fun
所指向的函数只有一个 read
函数,其实 *os.file
还包含 write
函数,也就是说 *os.file
其实还实现了 io.writer
接口。因此下面的断言语句可以执行:
var w io.writer w = r.(io.writer)
之所以用断言,而不能直接赋值,是因为 r
的静态类型是 io.reader
,并没有实现 io.writer
接口。断言能否成功,看 r
的动态类型是否符合要求。
这样,w 也可以表示成 <tty, *os.file>
,仅管它和 w
一样,但是 w 可调用的函数取决于它的静态类型 io.writer
,也就是说它只能有这样的调用形式: w.write()
。w
的内存形式如下图:
和 w
相比,仅仅是 fun
对应的函数变了:read -> write
。
最后,再来一个赋值:
var empty interface{} empty = w
由于 empty
是一个空接口,因此所有的类型都实现了它,w 可以直接赋给它,不需要执行断言操作。
从上面的三张图可以看到,interface 包含三部分信息:_type
是类型信息,*data
指向实际类型的实际值,itab
包含实际类型的信息,包括大小、包路径,还包含绑定在类型上的各种方法(图上没有画出方法),补充一下关于 os.file 结构体的图:
这一节的最后,复习一下上一篇关于 interface 的文章,提到的一个技巧,这里再展示一下:
先参考源码,分别定义一个“伪装”
的 iface 和 eface 结构体。
type iface struct { tab *itab data unsafe.pointer } type itab struct { inter uintptr _type uintptr link uintptr hash uint32 _ [4]byte fun [1]uintptr } type eface struct { _type uintptr data unsafe.pointer }
接着,将接口变量占据的内存内容强制解释成上面定义的类型,再打印出来:
package main import ( "os" "fmt" "io" "unsafe" ) func main() { var r io.reader fmt.printf("initial r: %t, %v\n", r, r) tty, _ := os.openfile("/users/qcrao/desktop/test", os.o_rdwr, 0) fmt.printf("tty: %t, %v\n", tty, tty) // 给 r 赋值 r = tty fmt.printf("r: %t, %v\n", r, r) riface := (*iface)(unsafe.pointer(&r)) fmt.printf("r: iface.tab._type = %#x, iface.data = %#x\n", riface.tab._type, riface.data) // 给 w 赋值 var w io.writer w = r.(io.writer) fmt.printf("w: %t, %v\n", w, w) wiface := (*iface)(unsafe.pointer(&w)) fmt.printf("w: iface.tab._type = %#x, iface.data = %#x\n", wiface.tab._type, wiface.data) // 给 empty 赋值 var empty interface{} empty = w fmt.printf("empty: %t, %v\n", empty, empty) emptyeface := (*eface)(unsafe.pointer(&empty)) fmt.printf("empty: eface._type = %#x, eface.data = %#x\n", emptyeface._type, emptyeface.data) }
运行结果:
initial r: <nil>, <nil> tty: *os.file, &{0xc4200820f0} r: *os.file, &{0xc4200820f0} r: iface.tab._type = 0x10bfcc0, iface.data = 0xc420080020 w: *os.file, &{0xc4200820f0} w: iface.tab._type = 0x10bfcc0, iface.data = 0xc420080020 empty: *os.file, &{0xc4200820f0} empty: eface._type = 0x10bfcc0, eface.data = 0xc420080020
r,w,empty
的动态类型和动态值都一样。不再详细解释了,结合前面的图可以看得非常清晰。
反射的基本函数
reflect 包里定义了一个接口和一个结构体,即 reflect.type
和 reflect.value
,它们提供很多函数来获取存储在接口里的类型信息。
reflect.type
主要提供关于类型相关的信息,所以它和 _type
关联比较紧密;reflect.value
则结合 _type
和 data
两者,因此程序员可以获取甚至改变类型的值。
reflect 包中提供了两个基础的关于反射的函数来获取上述的接口和结构体:
func typeof(i interface{}) type func valueof(i interface{}) value
typeof
函数用来提取一个接口中值的类型信息。由于它的输入参数是一个空的 interface{}
,调用此函数时,实参会先被转化为 interface{}
类型。这样,实参的类型信息、方法集、值信息都存储到 interface{}
变量里了。
看下源码:
func typeof(i interface{}) type { eface := *(*emptyinterface)(unsafe.pointer(&i)) return totype(eface.typ) }
这里的 emptyinterface
和上面提到的 eface
是一回事(字段名略有差异,字段是相同的),且在不同的源码包:前者在 reflect
包,后者在 runtime
包。 eface.typ
就是动态类型。
type emptyinterface struct { typ *rtype word unsafe.pointer }
至于 totype
函数,只是做了一个类型转换:
func totype(t *rtype) type { if t == nil { return nil } return t }
注意,返回值 type
实际上是一个接口,定义了很多方法,用来获取类型相关的各种信息,而 *rtype
实现了 type
接口。
type type interface { // 所有的类型都可以调用下面这些函数 // 此类型的变量对齐后所占用的字节数 align() int // 如果是 struct 的字段,对齐后占用的字节数 fieldalign() int // 返回类型方法集里的第 `i` (传入的参数)个方法 method(int) method // 通过名称获取方法 methodbyname(string) (method, bool) // 获取类型方法集里导出的方法个数 nummethod() int // 类型名称 name() string // 返回类型所在的路径,如:encoding/base64 pkgpath() string // 返回类型的大小,和 unsafe.sizeof 功能类似 size() uintptr // 返回类型的字符串表示形式 string() string // 返回类型的类型值 kind() kind // 类型是否实现了接口 u implements(u type) bool // 是否可以赋值给 u assignableto(u type) bool // 是否可以类型转换成 u convertibleto(u type) bool // 类型是否可以比较 comparable() bool // 下面这些函数只有特定类型可以调用 // 如:key, elem 两个方法就只能是 map 类型才能调用 // 类型所占据的位数 bits() int // 返回通道的方向,只能是 chan 类型调用 chandir() chandir // 返回类型是否是可变参数,只能是 func 类型调用 // 比如 t 是类型 func(x int, y ... float64) // 那么 t.isvariadic() == true isvariadic() bool // 返回内部子元素类型,只能由类型 array, chan, map, ptr, or slice 调用 elem() type // 返回结构体类型的第 i 个字段,只能是结构体类型调用 // 如果 i 超过了总字段数,就会 panic field(i int) structfield // 返回嵌套的结构体的字段 fieldbyindex(index []int) structfield // 通过字段名称获取字段 fieldbyname(name string) (structfield, bool) // fieldbynamefunc returns the struct field with a name // 返回名称符合 func 函数的字段 fieldbynamefunc(match func(string) bool) (structfield, bool) // 获取函数类型的第 i 个参数的类型 in(i int) type // 返回 map 的 key 类型,只能由类型 map 调用 key() type // 返回 array 的长度,只能由类型 array 调用 len() int // 返回类型字段的数量,只能由类型 struct 调用 numfield() int // 返回函数类型的输入参数个数 numin() int // 返回函数类型的返回值个数 numout() int // 返回函数类型的第 i 个值的类型 out(i int) type // 返回类型结构体的相同部分 common() *rtype // 返回类型结构体的不同部分 uncommon() *uncommontype }
可见 type
定义了非常多的方法,通过它们可以获取类型的一切信息,大家一定要完整的过一遍上面所有的方法。
注意到 type
方法集的倒数第二个方法 common
返回的 rtype
类型,它和上一篇文章讲到的 _type
是一回事,而且源代码里也注释了:两边要保持同步:
// rtype must be kept in sync with ../runtime/type.go:/^type._type.
type rtype struct { size uintptr ptrdata uintptr hash uint32 tflag tflag align uint8 fieldalign uint8 kind uint8 alg *typealg gcdata *byte str nameoff ptrtothis typeoff }
所有的类型都会包含 rtype
这个字段,表示各种类型的公共信息;另外,不同类型包含自己的一些独特的部分。
比如下面的 arraytype
和 chantype
都包含 rytpe
,而前者还包含 slice,len 等和数组相关的信息;后者则包含 dir
表示通道方向的信息。
// arraytype represents a fixed array type. type arraytype struct { rtype `reflect:"array"` elem *rtype // array element type slice *rtype // slice type len uintptr } // chantype represents a channel type. type chantype struct { rtype `reflect:"chan"` elem *rtype // channel element type dir uintptr // channel direction (chandir) }
注意到,type
接口实现了 string()
函数,满足 fmt.stringer
接口,因此使用 fmt.println
打印的时候,输出的是 string()
的结果。另外,fmt.printf()
函数,如果使用 %t
来作为格式参数,输出的是 reflect.typeof
的结果,也就是动态类型。例如:
fmt.printf("%t", 3) // int
讲完了 typeof
函数,再来看一下 valueof
函数。返回值 reflect.value
表示 interface{}
里存储的实际变量,它能提供实际变量的各种信息。相关的方法常常是需要结合类型信息和值信息。例如,如果要提取一个结构体的字段信息,那就需要用到 _type (具体到这里是指 structtype) 类型持有的关于结构体的字段信息、偏移信息,以及 *data
所指向的内容 —— 结构体的实际值。
源码如下:
func valueof(i interface{}) value { if i == nil { return value{} } // …… return unpackeface(i) } // 分解 eface func unpackeface(i interface{}) value { e := (*emptyinterface)(unsafe.pointer(&i)) t := e.typ if t == nil { return value{} } f := flag(t.kind()) if ifaceindir(t) { f |= flagindir } return value{t, e.word, f} }
从源码看,比较简单:将先将 i
转换成 *emptyinterface
类型, 再将它的 typ
字段和 word
字段以及一个标志位字段组装成一个 value
结构体,而这就是 valueof
函数的返回值,它包含类型结构体指针、真实数据的地址、标志位。
value 结构体定义了很多方法,通过这些方法可以直接操作 value 字段 ptr 所指向的实际数据:
// 设置切片的 len 字段,如果类型不是切片,就会panic func (v value) setlen(n int) // 设置切片的 cap 字段 func (v value) setcap(n int) // 设置字典的 kv func (v value) setmapindex(key, val value) // 返回切片、字符串、数组的索引 i 处的值 func (v value) index(i int) value // 根据名称获取结构体的内部字段值 func (v value) fieldbyname(name string) value // ……
value
字段还有很多其他的方法。例如:
// 用来获取 int 类型的值 func (v value) int() int64 // 用来获取结构体字段(成员)数量 func (v value) numfield() int // 尝试向通道发送数据(不会阻塞) func (v value) trysend(x reflect.value) bool // 通过参数列表 in 调用 v 值所代表的函数(或方法 func (v value) call(in []value) (r []value) // 调用变参长度可变的函数 func (v value) callslice(in []value) []value
不一一列举了,反正是非常多。可以去 src/reflect/value.go
去看看源码,搜索 func (v value)
就能看到。
另外,通过 type()
方法和 interface()
方法可以打通 interface
、type
、value
三者。type() 方法也可以返回变量的类型信息,与 reflect.typeof() 函数等价。interface() 方法可以将 value 还原成原来的 interface。
这里引用老钱《快学go语言第十五课——反射》的一张图:
总结一下:typeof()
函数返回一个接口,这个接口定义了一系列方法,利用这些方法可以获取关于类型的所有信息; valueof()
函数返回一个结构体变量,包含类型信息以及实际值。
用一张图来串一下:
上图中,rtye
实现了 type
接口,是所有类型的公共部分。emptyface 结构体和 eface 其实是一个东西,而 rtype 其实和 _type 是一个东西,只是一些字段稍微有点差别,比如 emptyface 的 word 字段和 eface 的 data 字段名称不同,但是数据型是一样的。
反射的三大定律
根据 go 官方关于反射的博客,反射有三大定律:
- reflection goes from interface value to reflection object.
- reflection goes from reflection object to interface value.
- to modify a reflection object, the value must be settable.
第一条是最基本的:反射是一种检测存储在 interface
中的类型和值机制。这可以通过 typeof
函数和 valueof
函数得到。
第二条实际上和第一条是相反的机制,它将 valueof
的返回值通过 interface()
函数反向转变成 interface
变量。
前两条就是说 接口型变量
和 反射类型对象
可以相互转化,反射类型对象实际上就是指的前面说的 reflect.type
和 reflect.value
。
第三条不太好懂:如果需要操作一个反射变量,那么它必须是可设置的。反射变量可设置的本质是它存储了原变量本身,这样对反射变量的操作,就会反映到原变量本身;反之,如果反射变量不能代表原变量,那么操作了反射变量,不会对原变量产生任何影响,这会给使用者带来疑惑。所以第二种情况在语言层面是不被允许的。
举一个经典例子:
var x float64 = 3.4 v := reflect.valueof(x) v.setfloat(7.1) // error: will panic.
执行上面的代码会产生 panic,原因是反射变量 v
不能代表 x
本身,为什么?因为调用 reflect.valueof(x)
这一行代码的时候,传入的参数在函数内部只是一个拷贝,是值传递,所以 v
代表的只是 x
的一个拷贝,因此对 v
进行操作是被禁止的。
可设置是反射变量 value
的一个性质,但不是所有的 value
都是可被设置的。
就像在一般的函数里那样,当我们想改变传入的变量时,使用指针就可以解决了。
var x float64 = 3.4 p := reflect.valueof(&x) fmt.println("type of p:", p.type()) fmt.println("settability of p:", p.canset())
输出是这样的:
type of p: *float64 settability of p: false
p
还不是代表 x
,p.elem()
才真正代表 x
,这样就可以真正操作 x
了:
v := p.elem() v.setfloat(7.1) fmt.println(v.interface()) // 7.1 fmt.println(x) // 7.1
关于第三条,记住一句话:如果想要操作原变量,反射变量 value
必须要 hold 住原变量的地址才行。
反射相关函数的使用
代码样例
网络上各种博客文章里使用反射的样例代码非常多,读过这篇文章后,基本没有看不懂的,哈哈!不过,我这里还是举一个例子,并讲解一番:
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.bool { 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 的比较情形,这里做一个总结:
类型 | 深度相等情形 |
---|---|
array | 相同索引处的元素“深度”相等 |
struct | 相应字段,包含导出和不导出,“深度”相等 |
func | 只有两者都是 nil 时 |
interface | 两者存储的具体值“深度”相等 |
map | 1、都为 nil;2、非空、长度相等,指向同一个 map 实体对象,或者相应的 key 指向的 value “深度”相等 |
pointer | 1、使用 == 比较的结果相等;2、指向的实体“深度”相等 |
slice | 1、都为 nil;2、非空、长度相等,首元素指向同一个底层数组的相同元素,即 &x[0] == &y[0] 或者 相同索引处的元素“深度”相等 |
numbers, bools, strings, and channels | 使用 == 比较的结果为真 |
一般情况下,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 进行相互转换。
最后温习一下反射三大定律:
- reflection goes from interface value to reflection object.
- reflection goes from reflection object to interface value.
- to modify a reflection object, the value must be settable.
翻译一下:
- 反射将接口变量转换成反射对象 type 和 value;
- 反射可以通过反射对象 value 还原成原先的接口变量;
- 反射可以用来修改一个变量的值,前提是这个值可以被修改。
参考资料
【*中文】https://zh.wikipedia.org/wiki/%e5%8f%8d%e5%b0%84_(%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%a7%91%e5%ad%a6)
【码洞老钱 反射】
【go官方博客 reflection】
【gctt译文,不错】https://mp.weixin.qq.com/s/dkgj_fa0smvpv69t5nv-7a
【json库 源码分析】
【reflect 代码例子和图比较好】
【反射使用讲得不错】
【接口和反射的关系 ,english】
【总结成知识点】
【type value】
【讲得比较清晰简单】https://www.lijiaocn.com/%e7%bc%96%e7%a8%8b/2017/11/06/golang-reflection.html
【deepequal】https://github.com/chasiny/blog/blob/master/blog/go/package/go-reflect-deepequal.md
【反射使用场景】
上一篇: 智能指针和二叉树(2):资源的自动管理