蛮荆

Go 的反射与三大定律

2023-03-30

概述

在计算机学中,反射 (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, 有三种情况例外:

  1. 零值表示没有值 (声明了但是未初始化)
  2. 如果 IsValid 方法返回 false, Kind 方法也返回 false
  3. 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{} 类型之外,也可以转换为具体的类型,这里列举一下 boolbytes, 其他类型以此类推:

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 只有满足以下两个条件时才可以被修改:

  1. 可寻址
  2. 可导出的结构体字段

如果 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{} 得到反射对象

反射_1

  • 通过调用 reflect.ValueOf 方法,获取到具体的值对应的 reflect.Value 对象
  • 通过调用 reflect.TypeOf 方法,获取到具体的值对应的 reflect.Type 对象

反射可以通过反射对象得到 interface{}

反射_2

  • 通过调用 reflect.Interface 方法,获取到具体的值对应的 interface{}

例如:

  1. 通过调用 reflect.Bool 方法,获取到具体的值对应的 bool
  2. 通过调用 reflect.Bytes 方法,获取到具体的值对应的 []byte

修改反射对象的前提是值必须可以被修改

反射_3

  • 通过调用 reflect.Set 方法,修改反射对象
  • 通过调用 mustbeassignable,检测反射对象是否可以修改
  • 通过调用 mustbeexported,确认反射对象是否可导出
  • 通过调用 assignTo 方法,修改反射对象并返回一个新对象
  • 以上过程中如果任意条件不满足,直接产生 panic

小结

基础数据类型对象和 反射对象 之间的完全转化需要两个步骤:

  1. 基础数据类型转换为 interface{}
  2. interface{} 转换为 反射对象

反过来,步骤正好逆向:

  1. 反射对象 转换为 interface{}
  2. interface{} 转换为基础数据类型

反射 的内部实现分析完了,最后来总结下 反射 的优缺点及应用场景。

优点

  • 避免硬编码,提高灵活性
  • 获取代码运行时能力,可以补足标准库缺失的能力,如动态修改值、判断是否实现接口、动态调用方法 (动态语言的特性) 等

缺点

  • 有一定的学习和使用成本
  • 降低代码可读性
  • 降低代码性能
  • 规避了编译器检查,可能造成潜在的运行时 panicBug

使用建议

  • 框架内部可以使用,比如标准库中的 encoding/json 包使用反射来解析数据类型,开源的 ORM 框架中使用反射来获取对象与数据表映射关系
  • 普通代码中不建议使用,尤其是位于 hot path 上面的业务代码

Reference

  1. 反射 - 维基百科
  2. Go 如何实现 implements
  3. Go 的面向对象编程

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。