前言 🔗
日常写业务时,我们很少会用到反射,导致大部分人对 go 的反射还比较陌生。 虽然并不推荐在业务代码中写反射代码,但是了解它,能够让我们更好的去理解许多框架的逻辑,以及能够让自己具备有初步实现一个通用第三方 SDK 的能力。
什么是反射 🔗
在计算机学中,反射是指计算机程序在运行时(runtime)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。
来自维基百科
在 Golang 中,其实就是编译时是 interface{},不知道其具体的类型,要在运行中反射获取类型、更新检查他们的值、
执行它们的方法,
反射能够干什么 🔗
反射是元编程的一个关键策略
- java spring
- golang json 序列化与反序列化
- go orm 框架
- …
反射的缺点 🔗
- 性能
- 可读性
- go 作为静态语言,编译时能检查出不少问题,但反射跳过这检查,可能在运行中 panic
Golang 反射解析 🔗
要搞清楚反射,得先简要了解一下 interface
Interface 🔗
定义 🔗
接口有两种定义:
-
eface: 空接口定义, 包含具体类型与数据
-
iface: 非空接口定义(实现了方法的接口),
// 位于 src/runtime/runtime2.go
type eface struct {
_type *_type // 具体的类型
data unsafe.Pointer // 数据
}
type iface struct {
tab *itab // 指向itab 的指针
data unsafe.Pointer // 数据
}
_type 就是所有类型最原始的元信息
type _type struct {
size uintptr // 类型占用内存大小
ptrdata uintptr
hash uint32
tflag tflag // 标记位,主要用于反射
align uint8 // 对齐字节信息
fieldAlign uint8
kind uint8 // 基础类型枚举值, 26 个基础类型, int,ptr,struct,interface
equal func(unsafe.Pointer, unsafe.Pointer) bool // 比较两个形参对应对象的类型是否相等
...
}
iface 中 itab 相对复杂,存放的是类型、方法等信息
// 位于 src/runtime/iface.go
type itab struct {
inter *interfacetype // 包装了一层*_type,代表接口类型,go 中 中基础类型slice,chan 的类型 都有定义,并且都是包装了一层*_type
_type *_type // 等同于 eface 中的 *_type, 具体对象的类型
link *itab
bad int32
inhash int32 // has this itab been added to hash?
fun [1]uintptr // 这里存的是指向第一个方法的指针,其他方法在这个地址后按字典序存储,偏移量即可
}
各种基础类型的类型 🔗
// 位于 src/runtime/type.go
type interfacetype struct {
typ _type
pkgpath name // 包路径
mhdr []imethod // 方法
}
type arraytype struct {
typ _type
elem *_type // array 上具体元素的类型
slice *_type
len uintptr
}
type chantype struct {
typ _type
elem *_type // channel 上具体元素的类型
dir uintptr
}
// ...
Interface 总结 🔗
一个具体的对象转换成 interface后,类型信息与数据会分别存在在 _type,data 两个指针中,不会丢失信息(所以后面反射能够还原出实际对象),如果这个具体的对象还实现了一些接口的函数,方法列表保存在 fun 指向的地址。
使用 interface 还有一个常见的坑: 判断一个 interface 是不是 nil,只要类型信息不为空,则这个 interface != nil
反射 🔗
此处分析的代码都在 src/reflect 包下
反射最基本的两个方法
TypeOf(i interface{}) Type // 对应着 interface 结构体中 *_type
ValueOf(i interface{}) Value // 结合了data指针与 *_type 类型信息
在继续解析代码前,我们可以先思考以下几个问题
- 为什么 Value 有 Interface() (i interface{}) 而 Type 没有
- Kind 与 Type 的区别
- 我们期望从 struct 通过反射获取到什么信息
Type 🔗
反射包下的 Type 是一个接口,其倒数第两个方法 common() rtype其实就是上文中的 _type
// 位于 src/reflect/type.go 下
type Type interface {
Align() int
FieldAlign() int
Method(int) Method // 返回类型方法集里的第 `i` (传入的参数)个方法
MethodByName(string) (Method, bool) // 通过名称获取方法
NumMethod() int // 方法个数
Name() string // 类型名字
Size() uintptr
String() string
Kind() Kind // 所属基础类型
Implements(u Type) bool
AssignableTo(u Type) bool
ConvertibleTo(u Type) bool // 能否转换 u
Comparable() bool
Bits() int
ChanDir() ChanDir // channel 类型的方向
IsVariadic() bool
Elem() Type // 返回内部子元素类型,只能由类型 Array, Chan, Map, Ptr, or Slice 调用
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 // 返回 map 的 key 类型,只能由类型 map 调用
Len() int
NumField() int
NumIn() int // 入参个数
NumOut() int // 出参个数
Out(i int) Type
common() *rtype // 与 interface 结构中 _type 一一对应
uncommon() *uncommonType // 与 interface 结构中 itab 所包含内容对应,所具备的方法,包名
}
Type 的方法比较多, 个人认为下面几个比较常用,可以仔细去看一下具体代码
-
MethodByName(string) (Method, bool) 通过名称获取方法
-
FieldByName(name string) (StructField, bool) 根据名字获取struct中的字段信息,其中常用到字段信息包括字段名,类型,tag
-
Elem() Type 如果这个 type 是 Array, Chan, Map, Ptr, or Slice,返回其指向的具体元素的类型
-
AssignableTo(u Type) bool 判断两个类型的值能够直接赋值(类型相等,或者其中一个类型没有定义)
-
ConvertibleTo(u Type) bool 判断两个类型 是否能够强制转换, 如 Int 之间,int 与 float,string 与 int32 能够转换。
其实 Type 接口的之所以具备这么多方法,是为了使用便利与通用性。实际上很多都是特异化的,例如常用到的 Field(i int), 只有基础类型 为 Struct 的才能使用,Elem() 只能由类型 Array, Chan, Map, Ptr, or Slice 调用, 其他的类型调用会直接 panic。
下面的代码展示了struct 的解析,我们以及能够 struct 字段里面 字段名、类型、tag 等信息
// 位于/reflect/type.go 下 struct 类型的结构
type structType struct {
rtype
pkgPath name
fields []structField // sorted by offset
}
func (t *rtype) FieldByName(name string) (StructField, bool) {
if t.Kind() != Struct {
panic("reflect: FieldByName of non-struct type " + t.String())
}
tt := (*structType)(unsafe.Pointer(t))
return tt.FieldByName(name)
}
// A StructField describes a single field in a struct.
type StructField struct {
Name string // 字段名
PkgPath string // 包路径
Type Type // 字段类型
Tag StructTag // 字段 tag
Offset uintptr // offset within struct, in bytes
Index []int // index sequence for Type.FieldByIndex
Anonymous bool // is an embedded field
}
总结
TypeOf(i interface) 返回一个接口,通过这些接口的方法能够获取所有类型信息
Value 🔗
Value 是一个结构体, 其包含类型与数据信息
type Value struct {
typ *rtype // 类型
ptr unsafe.Pointer // 实际数据
flag // 标记位
}
Value 常用的方法
-
Type() Type 返回具体类型
-
Interface() (i interface{}) value 有类型又有数据,可以直接转化成 interface
-
MethodByName(name string) Value 返回方法
-
FieldByName(name string) Value 根据字段名找出字段对应的值,在 Type 有类似的方法,不过 Type 返回的是字段类型信息
-
Indirect(v Value) Value 这个 value 如果是指针,返回它指向的 value,不是返回本身,实际使用 Elem() 实现
-
Elem() Value 返回指向的对象,并且会标记为 addr, 返回的 value 能用Addr() 获取地址
-
CanSet() bool 判断 value 里的数据能否被改变,满足可寻址的条件(CanAddr),如果是字段是对外暴露的(字段名大写)
-
Convert(t Type) Value 改变 value 的具体类型,可以利用上面Type中提到的 ConvertibleTo方法 来判断是否可以转换,如int -> int64
-
Call(in []Value) []Value 调用 Call Value Kind必须是函数,用 in 作为参数,返回 []Value, 反射执行方法
-
取出 value 中的值转换成具体类型
Float() float64 取出 float得值
Int() int64 取出 int64 的值
…
-
改变 value 的值
Set(x Value) 把 value 里的值设置成 x, value 满足CanSet()
SetInt(x int64) 作用类似
SetLen(n int) 切片设置长度,不是切片 panic
总结
ValueOf(i interface) 返回一个结构体,这个结构体包含 <类型,数据> 信息, 并且提供了许多方法,来获取,修改里面的信息(类型,数据)
代码实战 🔗
能否开发一个 copy 函数完成对象之间的复制函数 🔗
web 开发PO 转化成 DTO 的代码必不可少,这些代码比较重复枯燥,写个方法提升下效率
func Copy(dest, src interface{}) error {
// dest 预期是指向对象的ptr,Go 中是值传递,不是 ptr 的话,修改会无效
// Indirect 如果这个值是指针,会返回指向的值,其中 flag 会加上 CanAddr 的标记
destValue := reflect.Indirect(reflect.ValueOf(dest))
if !destValue.CanAddr() {
return fmt.Errorf("dest type is not ptr")
}
// 反射 src 得到 src 的类型 与 对象值
srcValue := reflect.ValueOf(src)
srcType := reflect.TypeOf(src)
if srcType.Kind() == reflect.Ptr {
srcType = srcType.Elem()
}
// 遍历 src类型字段
for i := 0; i < srcType.NumField(); i ++ {
fieldSrc := srcType.Field(i)
// 根据 src 的字段名 在 dest 的结构中查找
fieldDest, exist := destValue.Type().FieldByName(fieldSrc.Name)
// dest 不存在同名的字段跳过
if !exist {
continue
}
// 判断 src 字段类型能否转化成 dest 字段类型
if ok := fieldSrc.Type.ConvertibleTo(fieldDest.Type); ok {
// 获取 src value 对象中具体的字段值,并且转换成 dest 字段的类型
convertValue := srcValue.FieldByName(fieldSrc.Name).Convert(fieldDest.Type)
// 设置 dest value 对象的相应字段的值
destValue.FieldByName(fieldSrc.Name).Set(convertValue)
}
}
return nil
}
上面就是一个对象 Copy 的主要代码,依靠字段名复制,主要问题有两个
- 完全依靠字段名匹配,字段名不一致且不好改了,能否完成复制
- 字段类型转换完全依靠内置的ConvertibleTo(u Type) bool 函数,如果我想完成 int64 -> time.time 的复制该怎么做
当然这个代码已经有比较好的实现了,见 https://github.com/jinzhu/copier, 目前 2.2 k star, 如此简单的思想,但又如此实用,是不是对反射的代码更有兴趣了呢。
orm 中需要对数据库表与实体model做一个映射,如何实现? 🔗
// Schema 解析 model 得到的元数据
type Schema struct {
Model interface{} // 实体对象
Name string // 实体的名字,作为表名
Fields []*Field // 字段列表,转化成 sql 中用到的信息
FieldNames []string // 字段名列表,这里用实体字段名作为 sql 中字段名
fieldMap map[string]*Field // map[字段名] 字段
}
// Field sql 表中字段
type Field struct {
Name string // sql 字段名
Type string // sql 类型 int bigint..
Tag string // 额外信息,例如 primary key
}
// model 作为与数据库表一一对应的实体,dialect 代表一种类型转换规则,例如 go -> mysql 中 string -> varchar
// 而 go -> sqlite中, string -> text
func Parse(model interface{}, dialect dialect.Dialect) *Schema {
// 获取 model 的实际类型
modelType := reflect.Indirect(reflect.ValueOf(model)).Type()
schema := &Schema{
Model: model,
Name: modelType.Name(), // 类型信息中 有实体名字
fieldMap: map[string]*Field{},
}
// 遍历实体中每个字段
for i := 0; i < modelType.NumField(); i++ {
// 字段信息
structField := modelType.Field(i)
// 字段必须是不是匿名的和对外暴露的
if !structField.Anonymous && ast.IsExported(structField.Name) {
// 根据 go 对象 构建 sql 表中字段
field := &Field{
Name: structField.Name, // 字段的名字
Type: dialect.DataTypeOf(reflect.Indirect(reflect.New(structField.Type))),
// 这里把 go 中的类型转换成 sql 中的类型,dialect 有多个实现,例如 sqlite, mysql
}
// 从 structFiled 获取需要的 tag 信息
if v, ok := structField.Tag.Lookup("orm"); ok {
field.Tag = v
}
// 存入 schema 中
schema.Fields = append(schema.Fields, field)
schema.FieldNames = append(schema.FieldNames, field.Name)
schema.fieldMap[field.Name] = field
}
}
return schema
}
上面的代码就完成了一个 model 对象对数据库表的映射,主要有:
- 表名 -> 数据库表名
- 字段名 -> 数据库表字段名
- 字段类型 -> 数据库表字段的类型
- tag -> 字段的一些额外信息
这里的功能就是 gorm 框架 https://github.com/go-gorm/gorm/blob/master/schema/schema.go 的核心逻辑,对自己写一个 orm 是不是更有信心一点了呢。
课后作业 🔗
-
reflect DeepEqual 方法 源代码
-
json Marshal 方法 源代码
-
govalidator 自己实现一个参数校验的方法,
以上就是全部内容了
最后再来一个问题, 2021 年底,go 会在 1.18 版本中添加泛型,你们能说一下 interface 与 泛型的区别吗