写点什么

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

  • 2019-09-17
  • 本文字数:7974 字

    阅读完需:约 26 分钟

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

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

阅读索引

  • 什么是反射

  • 为什么要用反射

  • 反射是如何实现的

  • types 和 interface

  • 反射的基本函数

  • 反射的三大定律

  • 反射相关函数的使用

  • 代码样例

  • 未导出成员

  • 反射的实际应用

  • json 序列化

  • DeepEqual 的作用及原理

  • 总结

什么是反射

直接看维基百科上的定义:


在计算机科学中,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。


那我就要问个问题了:不用反射就不能在运行时访问、检测和修改它本身的状态和行为吗?


问题的回答,其实要首先理解什么叫访问、检测和修改它本身状态或行为,它的本质是什么?


实际上,它的本质是程序在运行期探知对象的类型信息和内存结构,不用反射能行吗?可以的!使用汇编语言,直接和内层打交道,什么信息不能获取?但是,当编程迁移到高级语言上来之后,就不行了!就只能通过 反射来达到此项技能。


不同语言的反射模型不尽相同,有些语言还不支持反射。《Go 语言圣经》中是这样定义反射的:


Go 语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,但是在编译时并不知道这些变量的具体类型,这称为反射机制。

为什么要用反射

需要反射的 2 个常见场景:


1、有时你需要编写一个函数,但是并不知道传给你的参数类型是什么,可能是没约定好;也可能是传入的类型很多,这些类型并不能统一表示。这时反射就会用的上了。


2、有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定。这时就需要对函数和函数的参数进行反射,在运行期间动态地执行函数。


在讲反射的原理以及如何用之前,还是说几点不使用反射的理由:


1、与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。


2、Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。


3、反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。

反射是如何实现的

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>,仅管它和 r 一样,但是 w 可调用的函数取决于它的静态类型 io.Writer,也就是说它只能有这样的调用形式: w.Write() 。


w 的内存形式如下图:



和 r 相比,仅仅是 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, )
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 字段,如果类型不是切⽚,就会panicfunc (v Value) SetLen(n int)// 设置切⽚的 cap 字段func (v Value) SetCap(n int)// 设置字典的 kvfunc (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(vValue) 就能看到。


另外,通过 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.4p := reflect.ValueOf(&x)fmt.Println("type of p:", p.Type())fmt.Println("settability of p:", p.CanSet())
复制代码


输出是这样的:


type of p: *float64settability of p: false
复制代码


p 还不是代表 x, p.Elem() 才真正代表 x,这样就可以真正操作 x 了:


v := p.Elem()v.SetFloat(7.1)fmt.Println(v.Interface()) // 7.1fmt.Println(x) // 7.1
复制代码


关于第三条,记住一句话:如果想要操作原变量,反射变量 Value 必须要 hold 住原变量的地址才行。


2019-09-17 18:422262

评论

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

外部数据的合规引入助力银行用户营销系统冷启动

易观分析

隐私计算

Facebook 开源 Golang 实体框架 Ent 现已支持 TiDB

Geek_2d6073

云时代,租电脑还是初创型企业最好的选择吗?

阿里云弹性计算

远程办公 无影云电脑 初创型企业

产品FAQ(常见问题)文档模版

小炮

产品 FAQ

汉化版postman

Liam

Jmeter Postman 接口测试 API swagger

Rust Cell 与RefCell,有啥区别?

非凸科技

深入垂直业务场景,SaaS版供应商业务协同管理系统促进企业与供应商高效协同

数商云

数字化转型 供应链系统

教你VUE中的filters过滤器2种用法

华为云开发者联盟

Vue 过滤器 filters过滤器 组件过滤器 全局过滤器

昇思MindSpore行至2022,开源社区成就生态共赢

这不科技

华为 昇思MindSpore

软件定义存储厂商大道云行加入龙蜥社区

OpenAnolis小助手

生态 存储技术 龙蜥社区 大道云行 CLA

从二十年开源经历出发,70 后大龄程序员谈成长、困境与突围

TDengine

数据库 tdengine 开源

下拉推荐在 Shopee Chatbot 中的探索和实践

Shopee技术团队

算法 chatbot 推荐算法

OpenHarmony标准设备应用开发(三)——分布式数据管理

OpenHarmony开发者

OpenHarmony 分布式数据

后端开发—一文详解网络IO模型

Linux服务器开发

reactor 后端开发 Linux服务器开发 网络io 网络模型

《LeetCode 刷题报告》题解内容Ⅱ

謓泽

3月月更

叮咚!参与微服务免费试用,有机会获得腾讯内推资格!

InfoQ写作社区官方

腾讯云 微服务 热门活动

适合 Kubernetes 初学者的一些实战练习 (三)

汪子熙

云原生 集群 Kubernetes 集群 Kubernetes, 云原生, eBPF 3月月更

小程序开发入门教程

CRMEB

Tapdata 肖贝贝:实时数据引擎系列(六)-从 PostgreSQL 实时数据集成看增量数据缓存层的必要性

tapdata

数据库 实时数据

华为云GaussDB专家走进课堂,跟莘莘学子聊聊数据库

华为云数据库小助手

GaussDB GaussDB(for openGauss) GaussDB(for MySQL)

春暖花开,等你而来!4月月更挑战开始啦!

InfoQ写作社区官方

热门活动 4月月更

如何快速实现持续交付

阿里云云效

云计算 阿里云 软件开发 CI/CD 持续交付

明天直播:如何测试硬件设备与龙蜥操作系统的兼容性?

OpenAnolis小助手

硬件 直播 开源社区 sig 兼容性

利用 IoTDB 替换 OpenTSDB,服务大唐集团60家电厂,减少95%运维成本

Apache IoTDB

Apache IoTDB

Git教程-帮助开发人员更好的运用Git | 云效

阿里云云效

git 云计算 阿里云 DevOps 开发者

异构注册中心机制在中国工商银行的探索实践

SOFAStack

GitHub 开源 分布式架构 注册中心 工商银行

向工程腐化开炮 | 治理思路全解

阿里巴巴终端技术

Java android 腐化治理 工程腐化

week4作业

Asha

墨天轮访谈 | 华为云温云博:从客户视角出发,GaussDB(for Redis)究竟“香”在哪里?

墨天轮

数据库 redis 华为云 国产数据库 键值数据库

VuePress 博客搭建系列 33 篇正式完结!

冴羽

JavaScript Vue 前端 vuepress 博客搭建

一文带你了解 Python 中的迭代器

踏雪痕

Python 3月程序媛福利 3月月更

饶全成:深度解密 Go 语言之反射(上)_文化 & 方法_饶全成_InfoQ精选文章