Go语言学习 - reflect

1,527 阅读7分钟

Introduction

我一开始接触反射的时候对于这个名字感到非常鬼畜, "反射"两个字会让人联想到"条件反射", 进而联想到摇铃铛狗会流口水(wtf). 而事实上, 维基百科对于反射的定义是: 计算机能够在运行的时候检测/修改自身的状态. 这里着重强调了:

  • 运行时: 强调一个动态, 临场的感觉
  • 状态: "我"是什么, 有哪些属性, 有哪些函数

所以一句话来说的话:

  • 原因大概是: Go的编译器以及runtime, 会把类型信息+自身的值, 保存在程序中, 而reflect包负责提供一套访问接口
  • 原理大概是: 参考interface里的原理, 在现实中我们会用一个interface{}来 "接住"一个实例, 本质上是将这个实例, 转换成interface系统中的"eface". reflect包(后面简称ref)就基于这个eface结构, 来修改实例的值与类型.

类型系统

表类型ref.Type

反射包内用于描述类型的数据结构叫"rtype".尽管事实上, rtype能充分的描述出数据的类型信息, 但是我们在程序中调用ref.TypeOf()的时候并不返回rtype. 原因是我们不能, 直接把这个数据的底层类型信息完完整整暴露给你随意操作, 这个数据就应该是只读的, 而不是可以拿出来随便改的, 其次, rtype本身也很复杂, 可读性就很低, 抽象出一个Type接口, 程序读起来也会更通顺.

假设你现在想判断一个数据的类型, 你会希望你的程序向下面一样简洁直观. 再思考一下如果你真的使用了rtype, 程序会写成什么样, 灾难.


typeA := reflect.TypeOf([]string{})
typeB := reflect.TypeOf(123)
data := 456

switch reflect.TypeOf(data) {
  case typeA:
      // ...
  case typeB:
      // ...
}

里类型 - ref.Kind()

与此同时ref包也为每一种基础类型(例如:数组型,字符串型等,比较基础的类型)封装了一个特定的结构, 你在程序中调用的 reflect.String, reflect.Array指的就是这个. 就好像type A struct{}的表类型是A, 但A也是一个结构体类型, 也是一个reflect.Struct


数据系统

实际上, Type的使用也没有那么频繁, 使用的更加频繁的ref.Value, 在一些情况下, 例如我们需要处理很多个相似的结构体(对应MySql中的很多张表), 这种时候我们希望有一些更加"笼统"的函数来帮助我们统一处理. Value往往会在这个时候发挥作用: 生成一个"笼统"的函数. 不同于Type系统, Value就真的只是一个struct:

  • type *rtype: 给定Value用于取类型
  • pre *unsafe.Pointer: 指向实际数据的指针
  • flag: 往下

Flag

在书上, 网上找了若干文章, 都没有能把这个flag说明白的, 然而搞笑的是Flag在若干场合都在被反复使用. 想要明白ref包的工作原理是一定绕不开flag的.

flag holds metadata about the value.

flag(fl代替)本质上是一个uintptr, uintptr本质上也是一个integer. 往下走一步, 如果是整数, 那么就可以用二进制表示, 如果是二进制数, 那么就能位运算了. 接下来的若干计算&表示, 就是基于这些二进制位运算的. 简单理解, 就是这些位运算的结果替代了一个一个的"if"语句, 返还一个我们想要的类型.

那怎么用一个整数说得清 "metadata"呢? 先将整数分成高位&低位

  • 低位, 用于表征基础类型 Kind() :
    • 基础类型Kind从bool型到struct型一共有27种
    • 我们需要至少5位用于表征类型: 2^4 + ... 2^0 = 31
  • 高位, 用于表征数据的特征:
    • 特征指: 只读/可寻址/是函数/..
    • 分别用以下内容表示
    • 1 << 5 == 0010000
    • 1 << 6 == 0100000
    • 1 << 7 == 1000000
    • ...

Flag低位: 如何得到Value的Kind()

// reflect包中Kind()的计算方法:
var (
    flagKindWidth = 5 
    flagKindMask  = 1<<flagKindWidth - 1
)
func (v Value) Kind() Kind {
	return v.kind()
}
func (f flag) kind() Kind {
	return Kind(f & flagKindMask)
}


// 测试脚本
func main() {
  
  v1 := 123
  vv := reflect.ValueOf(v1)
  
  v2 := false
  vv = reflect.ValueOf(v2)
  
  v3 := []string{}
  vv = reflect.ValueOf(v3)
}

先讲原理: 为了用于表征Kind, 我们使用5位, 因此flagKindWidth等于5, flagKindMask经过计算是 0...011111, 在kind()函数中, 我们要与Mask位运算, 而与Mask的与运算本质上是取后五位.也就是说: Value.Kind(), 本质上就是取flag的低五位

在说说测试脚本: 设置IDE中设置断点, 开debug查看flag值我们能得到以下信息:

  • v1: flag = 130, 二进制表示为 10000010, 经Mask运算,得(二进制)00010 == (十进制)2
  • v2: flag = 129, 二进制表示为 10000001, 经Mask运算,得(二进制)00001 == (十进制)1
  • v3: flag = 151, 二进制表示为 10010111, 经Mask运算,得(二进制)10111 == (十进制)23

那么2/1/23代表什么? 查看Kind()函数

const (
	Invalid Kind = iota  -> 0
	Bool                 -> 1
	Int                  -> 2
	...
	Slice                -> 23

破案.

Flag高位: 用于表征数据特征

func (v Value) CanAddr() bool {
	return v.flag&flagAddr != 0
}

flagAddr  = 1 << 8

取址的判断, 来自于Value的flag位与flagAddr的与运算结果. 其他的属性, 从第五位遍布到第九位, 到时候可以根据Value.flag与期望的属性做与运算即可判断对应数据的属性.

Flag位应用: ValueOf到底做了什么

我们在文章一开始就提出过, 反射的实现很大程度上依赖了接口实现中的eface以及rtype, 这里就是开始的地方: unpackEface函数 -> 解eface函数.

  • 首先生成一个"emptyInterface":
    • 在接口系统中, 当一个空接口"接住"了一个实例, 相关的参数(rtype)信息立刻就会生成. reflect的实现借助rtype信息将元信息对外暴露. 这句话里的"元信息"指的就是rtype信息.
  • 接下来, 我们取出rtype信息:
    • 拿出其中的Kind(), 也就是emptyInterface的低五位
    • 通过f |= flagIndir 设置上flagIndir(第7位)
  • 最后将刚刚组装出来的flag以及emptyInterface的数据部分(e.word)组装成一个新的Value
func ValueOf(i interface) Value {
  return unpackEface(i)
}

func unpackEface(i interface{}) Value {
	e := (*emptyInterface)(unsafe.Pointer(&i))
  t := e.typ
	f := flag(t.Kind())
	if ifaceIndir(t) {
		f |= flagIndir
	}
	return Value{t, e.word, f}
}	

type emptyInterface struct {
	typ  *rtype
	word unsafe.Pointer
}

Flag应用: 可取址性

CanAddr() 是反射包的内置函数, 用于表征这个Value是否为"可寻址的"."可寻址"是一个重要议题, 并不是所有内容在反射里都是可以寻址的. 举一个场景的例子, 用于数据库查询的GORM库, 在查询的时候需要将实例作为参数传递进去, 查询出来的结果会被存在这个参数里. 这种情况下, 自然而然会想到, 只有指针才能很好的满足这样的场景, 如果参数是不可寻址的, 自然也不会允许"内容存到指针里"这样的事情.

下面的例子展示如何获得一个可寻址的interface{}, 在第一步里取Value得到的flag仅仅是22(也就是reflect.Ptr)类型, 因为flagAddr位并没有设置上因此是不可寻址类型, 直接尝试Addr()会导致panic

func main() {
	ptr := reflect.ValueOf(&user) // 取Value -> 此时flagAddr=0
	return ptr.Addr().Interface() // PANIC !!!!!
}

因此我们需要Indirect步骤, 这一步去往Elem, 简单来说, 这一步的做法是, 如果Value底层是Ptr:

  • 先设置上可取址位: v.flag & flagRO | flagIndir | flagAddr, 这一步使得 RO(只读)位, Indir位, Addr位全都为1 -> 因此flagAddr为1, 可取址
  • 最后取出底层类型: fl |= flag(typ.Kind()), 设置上低五位
func main() {
	ptr := reflect.ValueOf(&user) // 取Value
	elem := reflect.Indirect(ptr) // 关键
	return elem.Addr().Interface()
}

func Indirect(v Value) Value {
	if v.Kind() != Ptr {
		return v
	}
	return v.Elem()
}

func (v Value) Elem() Value {
	k := v.kind()
	switch k {
	case Ptr:
		ptr := v.ptr
    ...
		fl := v.flag&flagRO | flagIndir | flagAddr
		fl |= flag(typ.Kind())
		return Value{typ, ptr, fl}
	}
	panic(&ValueError{"reflect.Value.Elem", v.kind()})
}

推荐阅读