golang 反射标准库 reflect 详解(一)

2023年 9月 22日 50.4k 0

反射基本概念:

反射指的是程序在运行期间动态地获取变量类型信息和值信息,同时也可以动态地对变量进行修改。

这个概念听起来不是很好理解,我觉得可以从以下几方面考虑:

动态:

动态是针对于go语言本身是一个静态语言而言的,这主要指go语言是一个强类型的编译型语言,在编译期间所有变量的类型就是确定好的,运行时不会发生变化,当声明一个函数时,完全不用考虑运行时会被赋值为另一个类型的变量。

func demo(a int64) {
  fmt.Println(a)
}

对于函数demo而言,参数a的类型为int64,运行期间a不可能变为另一种类型。

但go中也存在一些动态元素,实际上指的就是接口,当一个变量的类型为接口时,实现该接口的类型的变量都可以赋值给这个接口变量,其中比较特殊的空接口,不需要具体类型实现任何方法,就可以直接赋值给空接口变量,这也就导致接口中具体包含的变量类型是不确定的,在运行期间可以发生改变,这种运行时类型发生变化的现象就是动态。

func main() {
	demo(1)
	demo(1.5)
	demo("123456")
}

func demo(a interface{}) {
	fmt.Println(a)
}

当a的变量类新为空接口时,运行时可以将不同类型的值赋值给该变量。

获取类型和值信息:

基于上述所说,一个接口中具体的变量信息是未知的,那么至少要提供一种方式,能够动态的获取接口中包含的具体变量的类型信息和值信息,并且要能够进行设置,否则即使具备动态的特性,也无法发挥作用,反射就是这样一种能力。当一个变量是一个空接口变量时,就可以对这个接口变量进行反射,从而获取具体的变量类型,示例如下:

func main() {
    demo(1)
    demo(1.2)
    demo("1")
}

func demo(a interface{}) {
    // a变量是一个空接口,其中具体的类型是不确定的
    // 这里使用反射,动态获取a中包含的类型信息
    aType := reflect.TypeOf(a)
    fmt.Println(aType)
}

// output:
// int
// float64
// string

这个例子可能让人产生一种误解,就是只能对空接口进行反射,但并非如此,我们完全可以对一个固定类型的变量进行反射,

func main() {
    var a int64 = 1
    aType := reflect.TypeOf(a)
    fmt.Println(aType)
}

// output:
// int64

但我想说的一点是如果一个变量的类型是确定的,那么使用反射的必要性可能不太大,因为我们已经知道了这个变量的类型信息,并且也不会发生改变,那么最好不要徒增代码的复杂度。还有一点就是反射包中的函数如TypeOf的参数入参也是interface{},也就是说具体类型在进行反射值也会将其赋值给一个空接口,这和反射具体的实现原理有关,所以可以肯定的一点是,反射是始终和接口有关的,对于反射的认知不能仅仅是声明一个变量,之后使用reflect包的api获取其类型信息和值信息,这样并未发挥反射的能力,反射更多还是在动态地场景下结合空接口使用。

何时会用到反射:
比如现在在开发一个底层的框架,或者是一个泛用性很强的工具函数,无法对函数的参数类型做出假设,或者是无论外部传入何种类型的变量,该函数都要支持,这种情况下这个函数的参数一般只能设置为一个interface{},之后函数内部再具体使用反射获取类型信息和值信息,基于不同的类型和值做出不同的处理,最为典型的比如fmt.Println()或者json.Marshal(),这些函数无论外界传入何种类型的变量,都要能合理地做出处理,故这些函数的参数均为interface{},内部则使用反射进行实现,动态获取具体的变量类型信息、值信息,之后按照不同的类型进行信息的打印、json序列化的处理。

可以简单看一下Println的实现,只截取了很少一部分代码:

func (p *pp) doPrintln(a []any) {
	for argNum, arg := range a {
		if argNum > 0 {
			p.buf.writeByte(' ')
		}
		// 依次打印每一个参数
		p.printArg(arg, 'v')
	}
	p.buf.writeByte('\n')
}

这些参数的类型都是interface{}(go 1.18有了泛型之后,标准库的参数类型为interface{}的都改为了any),故具体打印参数时,需要针对不同的类型做不同的处理,

switch f := arg.(type) {
// 优先使用类型断言,针对具体类型进行打印
case bool:
	p.fmtBool(f, verb)
case float32:
	p.fmtFloat(float64(f), 32, verb)
case float64:
	p.fmtFloat(f, 64, verb)
case complex64:
	p.fmtComplex(complex128(f), 64, verb)
case complex128:
	p.fmtComplex(f, 128, verb)
case int:
 ...
default:
  // 非内建类型或简单类型,则使用反射的方式动态地获取类型和值的信息从而进行打印
	// If the type is not simple, it might have methods.
	if !p.handleMethods(verb) {
		// Need to use reflection, since the type had no
		// interface methods that could be used for formatting.
    // 这里获取变量的反射值,针对值反射对象进行信息的打印
		p.printValue(reflect.ValueOf(f), verb, 0)
	}

}

这里优先采用类型断言的方式,类型断言也可以判断出一个接口中实际包含的变量的类型信息,相比较于使用反射,类型断言更快一些,但是却远没有使用反射灵活,使用类型断言时,断言的类型必须是已知的类型,比如标准库这里只能断言为一些go内建的类型,如果外部传入的是自定义类型的变量,比如自定义了一个结构体,使用类型断言就无能为力了,因为标准库肯定没有办法拿到开发人员自己定义的结构体,这时就会走到default分支,这里的printValue就是基于反射实现的。

// 依据于变量的种类进行打印
switch f := value; value.Kind() {
	case reflect.Invalid:
		if depth == 0 {
			p.buf.writeString(invReflectString)
		} else {
			switch verb {
			case 'v':
				p.buf.writeString(nilAngleString)
			default:
				p.badVerb(verb)
			}
		}

	...
  
	// 针对于结构体而言,对这类变量进行format
  case reflect.Struct:
	  if p.fmt.sharpV {
		  p.buf.writeString(f.Type().String())
	  }
    // 写入 '{'
	  p.buf.writeByte('{')
    // 遍历结构体的所有字段
	  for i := 0; i  0 {
			  if p.fmt.sharpV {
			  	p.buf.writeString(commaSpaceString)
			  } else {
				 p.buf.writeByte(' ')
			  }
		  }
		  if p.fmt.plusV || p.fmt.sharpV {
      // 写入结构体的字段名
			if name := f.Type().Field(i).Name; name != "" {
				p.buf.writeString(name)
				p.buf.writeByte(':')
			}
		}
    // 递归地打印结构体的每一个字段
		p.printValue(getField(f, i), verb, depth+1)
	}
  // 写入 '}'
	p.buf.writeByte('}')

这里并未详细地解释fmt.Println的源码,只是想粗略地说明一下反射的一些使用场景,整体而言,无法对函数的参数类型做出假设,或者是无论外部传入何种类型的变量,该函数都要支持时一般会用到反射。

reflect库的整体设计:

在go中对一个变量进行反射,由类型反射和值反射两部分构成:

func main() {
	var a int
  // a变量的类型反射对象
	aType := reflect.TypeOf(a)
	fmt.Println(aType.Name())
	fmt.Println(aType.Size())
  // a变量的值反射对象
	aValue := reflect.ValueOf(a)
	fmt.Println(aValue.IsZero())
}

// output:
// int
// 8
// true

aType为变量a的类型反射对象,可以动态地获取该变量类型相关的信息,如这里类型的名称为int,该类型的长度为8字节,类型反射对象完全没有a的值相关的信息,也就是说a是被赋值为1还是赋值为10,对于类型反射而言是没有区别的,reflect.TypeOf()会直接取该变量的类型信息,并基于这些类型基础信息构造一个类型反射对象,对外暴露一些获取类型信息的方法。

aValue为变量a的值反射对象,值反射对象中既包含类型信息,也包含值信息,也包含类型信息是因为没有类型是没有办法进行赋值操作的,没有类型的值只是一段二进制数据,没有办法去解释这段数据的含义。值反射对象对外提供的方法主要是与值操作相关的,比如判断变量是不是零值,是不是nil,同时也可以动态地给变量赋值,这种对变量值进行读写操作的方法都是由值反射对象提供的,但由于值反射对象中也包含类型信息,故值反射对象可以直接使用Type()方法直接得到类型反射对象。

go将反射设计为类型和值两个对象,我个人认为是一种很直观、很易用的做法,因为go中的每一个变量都由一段二进制数据以及解释这段二进制数据的类型构成,反射的这两个核心对象对象直观地还原了底层的实现。数据类型是高级语言才有的概念,汇编层面是没有数据类型的概念的,汇编操作的是每一个地址,地址中包含着二进制数据,这段二进制数据没有类型的含义,具体是什么类型可以说取决于使用何种指令操作这段内存,比如ADD指令,就相当于认为这段内存的变量是整形,对其进行ADD操作,但也可以认为这段内存是一个字符,把这段内存当字符用,显然这是很危险的,但因为没有类型的概念,所以使用什么指令操作都可以。

类型系统则规定了如何解释一段内存,限制了哪些运算符可以操作这段内存,比如如下代码:

func main() {
	a := make([]int, 5, 10)
	bPointer := unsafe.Pointer(&a)
	bPtr := uintptr(bPointer)
	cPointer := unsafe.Pointer(bPtr + 8)
	dPointer := unsafe.Pointer(bPtr + 16)
	fmt.Println(*(*int)(bPointer))
	fmt.Println(*(*int)(cPointer))
	fmt.Println(*(*int)(dPointer))
}

// output:
// 1374389989072
// 5
// 10

声明了一个切片a,其长度为5,容量为10,之后将其强转为一个unsafe.Pointer,这是一种不安全的通用型指针,可以将任何一个指针强转为该指针,再转换为另一种指针,这相当于突破了类型限制,故认为是不安全的,所以这些函数和类型都在"unsafe"包中。如这段代码,声明了一个切片,将切片强转为unsafe.Pointer,则bPointer指向切片的首地址,之后将其转换为uintptr,使地址可以进行运算,之后cPointer指向了首地址之后8个字节所在的地址,dPointer指向了首地址之后16个字节所在的地址,之后再将这三个指针强转为*int指针,访问地址并打印结果。
从最后的输出来看,bPointer指针指向的是一段比较大的数字,目前看不出含义,但cPointer和dPointer指向的内容则比较明显,5和10分别是切片的容量和长度,故可以这样认为,我们使用*int指针强行解释了切片a首地址之后的24字节的内存,并且都用int去解释,最终第8到16字段是切片的长度,第16到24字节是切片的容量。

其实这是因为声明一个切片变量,变量中包含的是切片的头部信息,其结构是这样的:

type SliceHeader struct {
	Data uintptr   // 切片中元素数组所在的地址
	Len  int       // 切片长度
	Cap  int       // 切片容量
}

一共包含三部分,首先Data字段是切片实际包含的数据的地址,也就是说上面的bPointer指向的内存实际上是一个地址,所以将其转化为int后会是一个比较大的数字,之后第二个字段Len是切片的长度,Cap则是切片的容量,cPointer和dPointer分别指向这两个字段的值,使用int解释这两段内存,得到的就是切片的容量和长度。对于Data字段,可以使用如下代码验证:

func main() {
	a := make([]int, 5, 10)
	a[0] = 1
	a[1] = 4
	a[2] = 16
	// a的地址实际上也是首字段Data所在的地址,将其转化为*uintptr类型
	// 并访问,从而得到Data中保存的切片元素数组的首地址,也就是第一个元素
	// 所在的地址
	data1Ptr := *(*uintptr)(unsafe.Pointer(&a))
	// 添加偏移量,得到第二个元素以及第三个元素的地址,因为元素类型为int,64位机下
	// 占8个字节,故偏移量为8
	data2Ptr := data1Ptr + 8
	data3Ptr := data1Ptr + 16
	// 使用int类型去解释这三段内存,得到对应元素的值
	data1 := *(*int)(unsafe.Pointer(data1Ptr))
	data2 := *(*int)(unsafe.Pointer(data2Ptr))
	data3 := *(*int)(unsafe.Pointer(data3Ptr))
	fmt.Println(data1)
	fmt.Println(data2)
	fmt.Println(data3)
}

// output:
// 1
// 4
// 16

最终输出为1、4、16和最开始设置的切片的前三个元素保持一致,可以进一步说明,变量的值在内存中是一段连续的二进制数据,到底是何含义,需要用具体的类型去做解释(解释的具体操作在go中就是先将对应变量的指针强转为 unsafe.Pointer ,之后就可以将 unsafe.Pointer 转换为任意类型的指针,但这么做突破了类型的限制,是很不安全的,生产环境下不应轻易使用)。

基于这种思考,go中的反射也设计为了由类型反射和值反射两个核心对象完成,类型反射对象只包含该变量的类型信息,并提供了获取这些类型相关信息的方法,而值反射对象既包含该变量的类型信息,也包含指向该变量具体值的指针,并提供了获取值以及操作值的方法。

我们接下来考虑一下reflect库的整体实现原理,可以认为,反射的实现基本就是在考虑reflect对象该如何获取变量的类型信息和值信息,如何在对象内存储这些信息,又如何基于这些信息对外提供方法。

反射的基本原理:

获取变量的类型信息和值信息:

首先有一点需要明确,go作为一个静态语言,变量保存在内存时并不会同时保存变量的类型信息,只会保存变量的二进制数据,比如之前所说的切片,变量是一个
切片头,切片头由三个字段构成,分别是指向切片中元素数据的指针,切片的长度以及切片的容量,共占24个字节,每偏移8个字节,就可以访问到下一个字段的值,也就是说该变量所对应的内存中并没有某一部分会额外保存这个变量是什么类型,对于一个静态语言,也确实是不需要的,变量的类型并不会在运行时发生变化,也就没有必要为每一个变量去单独记录变量的类型。而一些动态语言,比如php,python,变量在运行时则可以赋值为不同类型的值,实际上php,python中变量的值都以堆中的对象的形式存在,对象中会有字段去记录变量当前的类型是什么,变量赋值为不同类型的值时字段也会发生改变。但go的变量是不会记录类型信息的,正因如此,go中的变量占用内存更小,程序执行效率也更高。

但如果go中的变量并没有保存类型信息,反射对象构造时又要如何获取类型呢,实际上正如最开始所说go语言中还是有一些动态属性的,也就是接口,接口中包含的实际变量的类型可以在运行时发生改变,那么接口中就必然包含了当前变量的类型信息,也就是说如果能先将一个变量赋值给一个接口,之后再从接口中获取类型信息和值信息就可以了,reflect库其实也确实这样做的,reflect库核心的两个构造函数TypeOf()以及ValueOf()的源码如下:

func TypeOf(i any) Type {
	eface := *(*emptyInterface)(unsafe.Pointer(&i))
	return toType(eface.typ)
}

func ValueOf(i any) Value {
	if i == nil {
		return Value{}
	}
  
  escapes(i)

	return unpackEface(i)
}


func unpackEface(i any) 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}
}

这两个函数的入参都是interface{},也就是说,外界函数调用这两个函数时都会将变量先赋值到一个空接口之上,TypeOf()以及ValueOf()基于这个空接口形参就可以获取实参的类型信息,这两个函数的形参类型为空接口绝对不仅仅是因为所有类型的变量都要使用这两个函数,所以使用空接口从而不对外部传入的参数类型做限制,还有一个很重要的原因就是空接口中含有着实参的类型信息和值信息。

接下来可以整体看一下这两个函数的实现原理:

这两个函数都有一处核心的代码:

eface := *(*emptyInterface)(unsafe.Pointer(&i))

// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
	typ  *rtype
	word unsafe.Pointer
}

使用指针强转的方式,将入参i装换为了emptyInterface类型,这个emptyInterface并不是随便一个结构体,这个结构体实际上就是空接口对应的结构体,之所以要强转这么一次,是因为go在语法层面,没有提供获取interface{}内部字段的方式,reflect库想要获取其内部包含的类型信息和值信息,只能手动写了一份空接口对应的结构体,之后将空接口强转为这个结构,基于这种方式就可以拿到空接口内部的字段。

我们可以手动的做一下这个实现,将这个结构体复制出来,然后也将一个空接口强转为这个结构体,观察一下其中的字段,看看是否基本符合预期:

type rtype struct {
size uintptr
ptrdata uintptr // number of bytes in the type that can contain pointers
hash uint32 // hash of type; avoids computation in hash tables
tflag tflag // extra type information flags
align uint8 // alignment of variable with this type
fieldAlign uint8 // alignment of struct field with this type
kind uint8 // enumeration for C
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
gcdata *byte // garbage collection data
str nameOff // string form
ptrToThis typeOff // type for pointer to this type, may be zero
}

type nameOff int32 // offset to a name
type typeOff int32 // offset to an *rtype
type tflag uint8

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

type Demo struct {
A string
B string
}

func main() {
demo := Demo{A: "a", B: "b"}
var i interface{} = demo
eface := *(*emptyInterface)(unsafe.Pointer(&i))
kindMask := (1

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论