HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

饶全成:深度解密 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:422170

评论

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

初识Golang之函数及方法的多返回值

Kylin

3月日更

智慧公安一键扫描二维码报警定位系统

13828808769

智慧交通

EGG Network阿凡提的模式是怎么样的?早点了解别错失这个机会!

币圈那点事

区块链

要求输出事故报告,线上日志文件却不见了!!

陈皮的JavaLib

Java 运维 日志框架

云端数智新引擎,腾讯云原生数据湖计算重磅发布

腾讯云大数据

大数据 数据湖

聊聊LiteOS中生成的Bin、HEX、ELF三种文件格式

华为云开发者联盟

编译器 LiteOS Bin HEX ELF

区块链产品宗谱链,一款记录族谱的APP

13828808769

区块链+ #区块链#

MindSpore实践:对篮球运动员目标的检测

华为云开发者联盟

深度学习 mindspore 图像检测 yolo 篮球运动

未来几年,低代码开发平台会如何发展?

优秀

低代码

6大创新技术及2亿美元投入计划,这个活动有点料

华为云开发者联盟

人工智能 数据库 华为 云原生 HDC.Cloud

数字化进入深水区

鲸品堂

方法论 数字化 企业数字化转型

基于NB-IoT的智慧路灯监控系统(NB-IoT专栏—实战篇5:手机应用开发)

不脱发的程序猿

物联网 28天写作 3月日更 NB-IoT智慧路灯 手机应用开发

Python基础之:Python中的IO

程序那些事

Python 人工智能 数据分析 程序那些事

基于深度学习的端到端通信系统模型

华为云开发者联盟

深度学习 端到端 编码器 通信系统 信道模型

v01.12 鸿蒙内核源码分析(双向链表) | 谁是内核最重要结构体 | 百篇博客分析 HarmonyOS 源码

鸿蒙研究站

鸿蒙 HarmonyOS 鸿蒙内核源码分析 百篇博客分析鸿蒙 百万汉字注解鸿蒙

另类数据:投资中的怪咖

博文视点Broadview

区块链电子合同签署平台,助力企业数字化转型

13828808769

区块链+ #区块链#

大“食”代来临,后厨重地可以更“聪明”点儿

IoT云工坊

人工智能 物联网 PaaS 智慧厨房 智慧餐饮

Python OpenCV 图像平移,取经之旅第 10 天

梦想橡皮擦

3月日更

17张图带你搞懂ZooKeeper一致性原理!

Java小咖秀

程序员 TCP udp 传输协议

金三银四了!必知必会,HTTP面试题!漫画图解超硬核!

小白debug

面试 网络编程 网络 HTTP 网络层

关于企业容器安全问题的思考

阿里巴巴中间件

金三银四,冰河为你整理了这份20万字134页的面试圣经!!

冰河

面试 面经 offer 金三银四 我要进大厂

降维打击:数据可视化降本增效,传统制造业价值即将扭转!

一只数据鲸鱼

物联网 数据可视化 工业物联网 数字化运维 3D

智能安防监控系统的发展与应用

anyRTC开发者

android 监控 音视频 WebRTC RTC

是谁拖(慢)了 Redis 的后腿?

escray

redis 学习 极客时间 3月日更 Redis 核心技术与实战

为什么很多工程师不了解Serverless

云原生

Serverless 云原生 Knative

智能化软件开发微访谈·第十六期:低代码/无代码开发

吴盛

低代码 快速开发 sql 无代码开发

35岁了,还不知道,TCP为什么会粘包?【硬核图解】

小白debug

TCP 网络 协议栈 TCP/IP 网络层

风暴眼中的“以太坊”堪比堵车的北京东三环,NA公链(Nirvana)NAC公链对垒胜算几何?

区块链第一资讯

区块链

关于热力图数据上报清洗,我们做了一个有意思的尝试

阿里巴巴中间件

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