Go 的反射与三大定律
2023-03-30 Golang 并发编程 Go 源码分析 读代码
概述
在计算机学中,反射 (reflection) 是指计算机程序在运行时 (runtime) 可以访问、检测和修改它本身的状态或行为的一种能力。简单来说,反射就是程序在运行的时候能够观察并且修改自己的行为。
常见场景
Go 中 反射
常见的使用场景有 元数据编程,确认接口实现机制。
元数据编程
通过 反射
获取字段标签,实现简单的元数据编程,比如 ORM
框架中的 Model
属性。
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string `column:"username" type:"varchar(32)"`
Age int `column:"age" type:"tinyint"`
}
func main() {
var u User
t := reflect.TypeOf(u)
// 获取 Name 字段对应的数据表字段
if name, ok := t.FieldByName("Name"); ok {
fmt.Printf("column = %s, type = %s\n",
name.Tag.Get("column"),
name.Tag.Get("type"),
)
}
// 获取 Age 字段对应的数据表字段
if age, ok := t.FieldByName("Age"); ok {
fmt.Printf("column = %s, type = %s\n",
age.Tag.Get("column"),
age.Tag.Get("type"),
)
}
}
// $ go run main.go
// 输出如下
/**
column = username, type = varchar(32)
column = age, type = tinyint
*/
是否实现接口
Go 中没有关键字 instanceof
来确认一个类型是否实现了某个接口,不过可以通过 reflect
包提供的 API 来确认。
package main
import (
"fmt"
"reflect"
)
type CustomStr interface {
String() string
}
type CustomError struct{}
func (*CustomError) Error() string {
return ""
}
func (*CustomError) String() string {
return ""
}
type CustomStr2 interface {
CustomStr
}
type CustomStr3 interface {
String() string
}
func main() {
// 获取 error 类型
typeOfError := reflect.TypeOf((*error)(nil)).Elem()
// 获取 CustomError 结构体类型
customError := reflect.TypeOf(CustomError{})
// 获取 CustomError 结构体指针类型
customErrorPtr := reflect.TypeOf(&CustomError{})
// 判断 CustomError 结构体类型是否实现了 error 接口
fmt.Println(customError.Implements(typeOfError)) // false
// 判断 CustomError 结构体指针类型是否实现了 error 接口
fmt.Println(customErrorPtr.Implements(typeOfError)) // true
// 判断 CustomError 结构体指针类型 (通过 nil 转换来的) 是否实现了 error 接口
fmt.Println(customErrorPtr.Implements(reflect.TypeOf((*CustomStr)(nil)).Elem())) // true
// 判断 CustomError 结构体指针类型是否实现了 error 接口 (嵌套的)
fmt.Println(customErrorPtr.Implements(reflect.TypeOf((*CustomStr2)(nil)).Elem())) // true
// 判断 CustomError 结构体指针类型是否实现了 CustomStr3 接口
fmt.Println(customErrorPtr.Implements(reflect.TypeOf((*CustomStr3)(nil)).Elem())) // true
}
// $ go run main.go
// 输出如下
/**
false
true
true
true
true
*/
内部实现
我们来探究一下 reflect
的内部实现文件目录为 $GOROOT/src/reflect
,笔者的 Go 版本为 go1.19 linux/amd64
。
反射类型
反射类型相关的结构体和方法在 type.go
文件内定义。
Type 接口
Type
接口用于数据类型和对应的反射方法的抽象表示。
Type
类型是可以比较的,例如使用 ==
操作符,因此可以作为 map
数据类型的 key
, 如果两个 Type
值表示相同的类型,则它们相等。
每个数据类型对应的方法都是不一样的,反过来也一样,每个方法不一定适用于所有数据类型,具体的限制在标准库的文档中都有注释说明。
type Type interface {
// 适用于所有数据类型的方法
// 返回在内存中分配该类型时的对齐方式(以字节为单位)
Align() int
// 当作为结构体中的字段时,FieldAlign 返回类型时的对齐方式(以字节为单位)
FieldAlign() int
// 返回类型的方法集合中第 i 个方法 (如果 i 越界,会导致 panic)
// 对于非接口类型 T 或者 *T,返回的方法的 Type 和 Func 字段描述了一个函数,其第一个参数是接收方,只有导出的方法是可访问的
// 对于接口类型,返回的方法的 Type 字段给出了方法签名,没有接收方,Func 字段为 nil
// 返回的方法列表按照字典顺序排序
Method(int) Method
// 返回在类型的方法集合中名字对应的方法,根据是否存在对应方法,返回 true/false
// 对于非接口类型 T 或者 *T,返回的方法的 Type 和 Func 字段描述了一个函数,其第一个参数是接收方
// 对于接口类型,返回的方法的 Type 字段给出了方法签名,没有接收方,Func 字段为 nil
MethodByName(string) (Method, bool)
// 返回使用 Method 可访问的方法的数量
// 注意,NumMethod 只对接口类型计算未导出的方法
NumMethod() int
// 返回已定义类型在其包内的类型名称
// 对于没有定义的类型返回空字符串
Name() string
// 返回已定义类型的包路径,即唯一标识包的导入路径,如 "encoding/base64"
// 如果类型是预声明 (也就是内置类型) 的 (string, error) 或者未定义的 (*T, struct{}, []int, 或者非定义类型的别名, 例如: type A int)
// 则返回空字符串
PkgPath() string
// 返回存储给定类型值所需要字节数,类似于调用 unsafe.Sizeof()
Size() uintptr
// 返回类型的字符串表示
// 字符串表示可以使用缩短的包名 (例如: 用 base64 代替 "encoding/base64"),并且不保证在类型之间是唯一的
// 如果需要测试类型标识,请直接比较类型
String() string
// Kind 返回类型的特定表示,详情见 Kind 类型
Kind() Kind
// 返回类型是否实现了接口 u
Implements(u Type) bool
// 返回类型是否可以赋值给 u
AssignableTo(u Type) bool
// 返回类型是否可以转换为类型 u
// 即使 ConvertibleTo 返回 true, 转换过程依然可能发生 panic
// 例如: 一个 slice []T 可以转换为 *[N]T, 但是如果它的长度小于 N, 发生 panic
ConvertibleTo(u Type) bool
// 返回类型是否可以比较
// 即使 Comparable 返回 true, 比较过程依然可能发生 panic
// 例如: interface values 可以比较,但是如果 interface 对应的动态类型不支持比较,panic
Comparable() bool
// 只适用于某些类型的方法,具体取决于 Kind
// 具体的对应关系如下:
//
// Int*, Uint*, Float*, Complex*: Bits
// Array: Elem, Len
// Chan: ChanDir, Elem
// Func: In, NumIn, Out, NumOut, IsVariadic.
// Map: Key, Elem
// Pointer: Elem
// Slice: Elem
// Struct: Field, FieldByIndex, FieldByName, FieldByNameFunc, NumField
// 返回类型的大小(以位为单位)
// 如果类型不符合上述规则,panic
Bits() int
// 返回 channel 类型的方向
// 如果类型不是 channel, panic
ChanDir() ChanDir
// 返回函数类型的最后一个参数是否为可变参数,如果是,t.In(t.NumIn() - 1) 返回参数的隐式实际类型 []T
// 具体的例子:
//
// For concreteness, if t represents func(x int, y ... float64), then
//
// t.NumIn() == 2
// t.In(0) is the reflect.Type for "int"
// t.In(1) is the reflect.Type for "[]float64"
// t.IsVariadic() == true
//
// 如果类型不是一个 Func, panic
IsVariadic() bool
// 返回类型的元素类型
// 如果类型不符合上述规则,panic
Elem() Type
// 返回结构体类型的第 i 个字段
// 如果类型不是结构体或参数 i 越界, panic
Field(i int) StructField
// 返回与索引对应的嵌套字段, 这相当于对每个索引 i 依次调用 Field
// 如果类型不是结构体, panic
FieldByIndex(index []int) StructField
// 返回结构体指定名称的字段,以及一个标记字段是否存在的 bool 值
FieldByName(name string) (StructField, bool)
// 返回满足匹配函数的名称的结构体字段,以及一个标记字段是否存在的 bool 值
// FieldByNameFunc 会考虑结构体本身的字段,以及嵌入到结构体中的字段
// 按照 BFS 算法,在包含满足匹配函数的一个或多个字段的最外层嵌套处停止 (BFS: 由浅入深)
// 如果该深度有多个字段满足匹配函数,它们将相互抵消,FieldByNameFunc 不返回任何匹配
// 这种行为反映了 Go 在包含嵌入字段的结构体中处理名称查找的方法
FieldByNameFunc(match func(string) bool) (StructField, bool)
// 返回函数类型的第 i 个参数的类型
// 如果类型不是函数或者 i 越界, 抛出 panic
In(i int) Type
// 返回 map 类型的 key 类型
// 如果类型不是 map, 抛出 panic
Key() Type
// 返回数组类型的长度
// 如果类型不是数组, 抛出 panic
Len() int
// 返回结构体类型的字段数量
// 如果类型不是结构体, 抛出 panic
NumField() int
// 返回函数类型的参数数量
// 如果类型不是函数, 抛出 panic
NumIn() int
// 返回函数类型的返回值数量
// 如果类型不是函数, 抛出 panic
NumOut() int
// Out 返回函数类型的第 i 个返回值
// 如果类型不是函数, 抛出 panic
// 如果 i 越界, 抛出 panic
Out(i int) Type
common() *rtype
uncommon() *uncommonType
}
Kind 类型
Kind
类型表示一种特定类型,零值时表示无效类型,该类型将 Go 语言中所有数据类型通过常量生成器的方式定义出来。
type Kind uint
const (
Invalid Kind = iota
Bool
Int
...
String
Struct
UnsafePointer
)
rtype 对象
rtype
对象表示反射类型,是一个 Type
接口的通用实现,被嵌入在其他数据类型对象内。
type rtype struct {
size uintptr // 类型占用内存大小
ptrdata uintptr // 类型可以包含指针的字节数
hash uint32 // 类型 Hash, 避免在 Hash 表中计算 (缓存的作用)
tflag tflag // 类型标志位
align uint8 // 内存对齐信息
fieldAlign uint8 // 类型结构体字段对齐字节数
kind uint8 // 类型的 Kind 表示
equal func(unsafe.Pointer, unsafe.Pointer) bool // 比较两个对象是否相等
gcdata *byte // GC 类型数据
str nameOff // 类型名称偏移量
ptrToThis typeOff // 类型指针偏移量
}
在 rtype
对象的基础上,基础数据结构通过内嵌,再依次单独定义,这里列举一下 数组
和 切片
, 其他类型以此类推:
// 数组类型
type arrayType struct {
rtype
elem *rtype
slice *rtype
len uintptr
}
// 切片类型
type sliceType struct {
rtype
elem *rtype
}
Typeof 函数
Typeof
函数返回参数 i 对应的动态类型的反射类型,如果参数 i 是一个 nil
, 返回 nil
。
func TypeOf(i any) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}
toType 函数
toType
函数将 *rtype
转换为一个反射类型,
func toType(t *rtype) Type {
if t == nil {
return nil
}
return t
}
反射值
反射值相关的结构体和方法在 value.go
文件内定义。
Value 对象
Value
对象表示值对应的 反射对象
, 提供了获取/设置值的方法。
在使用特定于值的方法之前,请先使用 Kind
方法查找值的适配性,调用不适配该类型的方法会导致运行时 panic
, 有三种情况例外:
- 零值表示没有值 (声明了但是未初始化)
- 如果 IsValid 方法返回 false, Kind 方法也返回 false
- String 方法返回 “”
Value
所表示的值可以被多个 goroutine
并发使用,当然前提是该值类型本身支持直接并发操作 (例如 map
就不支持并发写)。
type Value struct {
// typ 表示由值表示的值类型
typ *rtype
// 指向指的指针,如果设置了 flagIndir, 则是指向数据的指针
// 如果设置了 flagIndir 或 typ.pointers 方法返回 true 时有效
ptr unsafe.Pointer
// flag 持有 value 的元数据
// 最低位表示如下:
// - flagStickyRO: 通过未导出的非嵌入字段获得, 因此是只读的
// - flagEmbedRO: 通过未导出的嵌入字段获得, 因此是只读的
// - flagIndir: 持有指向数据的指针
// - flagAddr: v.CanAddr is true (表示设置了 flagIndir)
// - flagMethod: v 是方法值
// 接下来的 5 个 bits 给出值的 Kind 类型
// 除了方法值之外,它会重复 typ.Kind()
// 其余 23 位以上给出方法 values 的方法编号
// 如果 flag.kind()!= Func,代码可以假设没有设置 flagMethod
// 如果 ifaceIndir(typ),代码可以假设 flagIndir 已设置
flag
}
ValueOf 函数
ValueOf
函数返回一个存储在参数接口 i 中,初始化后的具体的新值。
func ValueOf(i any) Value {
if i == nil {
return Value{}
}
// 所有数据总是逃逸到堆上
// 控制生存周期更容易
escapes(i)
return unpackEface(i)
}
escapes 函数
escapes
函数保证参数变量逃逸到堆上。
// 学习小技巧: 控制变量逃逸到堆上
// Dummy 注解标记值 x 逃逸
func escapes(x any) {
if dummy.b {
dummy.x = x
}
}
var dummy struct {
b bool
x any
}
emptyInterface 对象
emptyInterface
对象表示一个 interface{} 值的头部。
type emptyInterface struct {
typ *rtype
word unsafe.Pointer
}
ifaceIndir 函数
ifaceIndir
函数返回参数 t 是否间接存储在一个 interface
值中。
func ifaceIndir(t *rtype) bool {
return t.kind&kindDirectIface == 0
}
unpackEface 函数
unpackEface
函数将一个 emptyInterface
对象转换为一个 Value
对象。
func unpackEface(i any) Value {
e := (*emptyInterface)(unsafe.Pointer(&i))
// 注意: 在确认 e.word 是否为一个指针之前,不要读取
// 避免赋值时 panic, Value 结构体第二个字段为 unsafe.Pointer
t := e.typ
if t == nil {
return Value{}
}
f := flag(t.Kind())
if ifaceIndir(t) {
f |= flagIndir
}
return Value{t, e.word, f}
}
CanInterface 方法
CanInterface
方法返回调用 Interface
方法是否不会产生 panic
, 虽然返回值是 bool
,但是函数内部却可能直接 panic
, 所以把兼容性处理的工作交给了调用方。
func (v Value) CanInterface() bool {
if v.flag == 0 {
panic(&ValueError{"reflect.Value.CanInterface", Invalid})
}
return v.flag&flagRO == 0
}
Interface 方法
将 Value
对象转换为一个 interface{}
类型返回 (Go 1.18 及之后是 any
类型,这里为了兼容,统一用 interface{}
来描述),如果 Value
对象是通过访问未导出的结构体字段获得的,抛出 panic
。
func (v Value) Interface() (i any) {
return valueInterface(v, true)
}
Value 转换为具体类型
Value
除了可以将转换为 interface{}
类型之外,也可以转换为具体的类型,这里列举一下 bool
和 bytes
, 其他类型以此类推:
func (v Value) Bool() bool {
if v.kind() != Bool {
v.panicNotBool()
}
return *(*bool)(v.ptr)
}
func (v Value) Bytes() []byte {
if v.typ == bytesType {
return *(*[]byte)(v.ptr)
}
return v.bytesSlow()
}
Elem 方法
Elem
方法返回参数 v 作为接口时包含的值,或参数 v 作为指针时指向的值,如果 v 的 Kind
类型不是接口或指针,产生 panic
, 如果 v == nil,返回 nil
。
func (v Value) Elem() Value {
k := v.kind()
switch k {
case Interface:
...
case Pointer:
...
}
panic(&ValueError{"reflect.Value.Elem", v.kind()})
}
CanSet 方法
CanSet
方法返回参数 v 是否可以被修改,一个 Value
只有满足以下两个条件时才可以被修改:
- 可寻址
- 可导出的结构体字段
如果 CanSet
方法返回 false, 调用 Set 方法或其他特定类型的 Setter 方法时 (例如 SetBool, SetInt 等),产生 panic
。
func (v Value) CanSet() bool {
return v.flag&(flagAddr|flagRO) == flagAddr
}
Set 方法
Set
方法将 Value
设置为参数 x, 和数据类型转换的基础通用规则一样,x 的值必须可以赋值给 v 的类型。
func (v Value) Set(x Value) {
v.mustBeAssignable() // 是否可以被设置
x.mustBeExported() // 是否对外公开
...
x = x.assignTo("reflect.Set", v.typ, target)
...
}
mustBeAssignable 方法
mustBeAssignable
方法用于检测赋值操作,如果不可赋值时产生 panic
。
func (f flag) mustBeAssignable() {
...
}
mustBeExported 方法
mustBeExported
方法用于检测是否可导出,如果不可导出时产生 panic
。
func (f flag) mustBeExported() {
...
}
assignTo 方法
assignTo
方法返回一个直接分配到 dst 的 Value
, 如果参数 v 是不可分配的,产生 panic
。
func (v Value) assignTo(context string, dst *rtype, target unsafe.Pointer) Value {
...
}
implements 方法
implements
方法返回类型 V 是否实现来接口 T。
func implements(T, V *rtype) bool {
if T.Kind() != Interface {
// 如果 T 不是接口类型
return false
}
t := (*interfaceType)(unsafe.Pointer(T))
if len(t.methods) == 0 {
// 空的接口,任意类型都自动实现该接口
return true
}
// 相同的算法适用于这两种情况,但是接口类型和具体类型的方法表不同,因此代码 (算法部分) 是重复的
// 在两种情况下,算法都是对两个列表: T 的方法和 V 的方法,同时进行线性扫描 (时间复杂度 O(N))
// 因为方法表是按照唯一的顺序存储的 (字典顺序,没有重复的方法名)
// 所以对 V 的方法的扫描必须与 T 的每个方法都匹配,否则 V 就没有实现 T
// 这样可以在线性时间内进行扫描,而不是单纯搜索所需的平方级复杂度
if V.Kind() == Interface {
// 接口类型判断
v := (*interfaceType)(unsafe.Pointer(V))
i := 0
for j := 0; j < len(v.methods); j++ {
...
if vmName.name() == tmName.name() && V.typeOff(vm.typ) == t.typeOff(tm.typ) {
...
if i++; i >= len(t.methods) {
return true
}
}
}
return false
}
// 普通类型判断
v := V.uncommon()
if v == nil {
return false
}
i := 0
vmethods := v.methods()
for j := 0; j < int(v.mcount); j++ {
...
if vmName.name() == tmName.name() && V.typeOff(vm.mtyp) == t.typeOff(tm.typ) {
...
if i++; i >= len(t.methods) {
return true
}
}
}
return false
}
三大定律
反射可以通过 interface{} 得到反射对象
- 通过调用
reflect.ValueOf
方法,获取到具体的值对应的reflect.Value
对象 - 通过调用
reflect.TypeOf
方法,获取到具体的值对应的reflect.Type
对象
反射可以通过反射对象得到 interface{}
- 通过调用
reflect.Interface
方法,获取到具体的值对应的interface{}
例如:
- 通过调用
reflect.Bool
方法,获取到具体的值对应的bool
- 通过调用
reflect.Bytes
方法,获取到具体的值对应的[]byte
修改反射对象的前提是值必须可以被修改
- 通过调用
reflect.Set
方法,修改反射对象 - 通过调用
mustbeassignable
,检测反射对象是否可以修改 - 通过调用
mustbeexported
,确认反射对象是否可导出 - 通过调用
assignTo 方法
,修改反射对象并返回一个新对象 - 以上过程中如果任意条件不满足,直接产生
panic
小结
基础数据类型对象和 反射对象
之间的完全转化需要两个步骤:
- 基础数据类型转换为
interface{}
interface{}
转换为反射对象
反过来,步骤正好逆向:
反射对象
转换为interface{}
interface{}
转换为基础数据类型
反射
的内部实现分析完了,最后来总结下 反射
的优缺点及应用场景。
优点
- 避免硬编码,提高灵活性
- 获取代码运行时能力,可以补足标准库缺失的能力,如动态修改值、判断是否实现接口、动态调用方法 (动态语言的特性) 等
缺点
- 有一定的学习和使用成本
- 降低代码可读性
- 降低代码性能
- 规避了编译器检查,可能造成潜在的运行时
panic
或Bug
使用建议
- 框架内部可以使用,比如标准库中的
encoding/json
包使用反射来解析数据类型,开源的ORM
框架中使用反射来获取对象与数据表映射关系 - 普通代码中不建议使用,尤其是位于
hot path
上面的业务代码