Go 反射的一些思考

什么是反射

首先要明确一点,程序的本质是代码 + 数据,我们编写代码的本质是为了控制数据、处理数据。在现代编程中,我们常常会有动态的东西,运行时才知道操作的数据是什么,无法编译时候确定,就需要反射。比如最常用的 json 序列化场景,我们直接 json.Marshal 就完事了,但是它是怎么实现的呢?其实使用的是反射(链接):

func marshal(v any) {
	reflectValue(reflect.ValueOf())
}

func reflectValue(v reflect.Value) {
	valueEncoder(v)()
}

func valueEncoder(v reflect.Value) encoderFunc {
	return typeEncoder(v.Type())
}

func typeEncoder(t reflect.Type) encoderFunc {
	f, loaded = encoderCache.LoadOrStore()
	if loaded {
		return f
	}
	f = newTypeEncoder(t)
	encoderCache.Store()
	return f
}

func newTypeEncoder(t reflect.Type) encoderFunc {
	switch t.Type() {
	case reflect.Bool:
		...
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		...
	case reflect.Struct:
		...
	...
	}
}

像这种 JSON 序列化的场景是不是非用反射不可?那也不一定,因为使用反射本质是编译的时候不能够确定字段的类型,struct 里面有什么字段,但如果编译时候能确定也是可以的,比如 zap 库实现的 json encoder

总的来说,反射就是运行时获取到数据的结构,并加以操作。很多无法编译时确定的逻辑都可以使用反射来实现。

反射是怎么做的

TypeOf

首先要理解 eface 和 iface。Go 里面的 interface 有两种形态,分别是 eface 和 iface,简单来说,当我们使用这个 interface 类型的变量不需要他对应的接口时,他就是 eface,否则是 iface。比如:

type MyInterface interface {
	Hello(word any)
}

type MyStruct {
}

func (MyStruct) Hello(word any) {
	fmt.Println(word)
}

func mian() {
	var a MyInterface = &MyStruct{}
	b := 1
	a.Hello(b)
}

在这个例子中,我们需要用到 a 对应 interface 的方法集,即 MyInterface,它的类型是 iface,而 b 仅作为一个值传递,word 是 eface。

说回正题,TypeOf 其实巧妙地利用了 runtime 里 eface 会保存数据的类型信息这个特性从而获取他的类型。在 runtime 里 eface 的结构是:

type eface struct {
	_type *_type
	data  unsafe.Pointer
}

_type 的结构是:

type _type struct {
	size       uintptr
	ptrdata    uintptr // size of memory prefix holding all pointers
	hash       uint32
	tflag      tflag
	align      uint8
	fieldAlign uint8
	kind       uint8
	// function for comparing objects of this type
	// (ptr to object A, ptr to object B) -> ==?
	equal func(unsafe.Pointer, unsafe.Pointer) bool
	// gcdata stores the GC type data for the garbage collector.
	// If the KindGCProg bit is set in kind, gcdata is a GC program.
	// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
	gcdata    *byte
	str       nameOff
	ptrToThis typeOff
}

当我们调用 reflect.TypeOf 时,传递的值在 runtime 会转成 interface{},再通过使用 unsafe.Pointer 强转成底层的存储结构就能获取到 eface 实际在内存上存储的数据了。题外话:曾看到 cgo 文档有一句话提到过未来 Go 可能会做 struct 字段排序优化,如果优化了可能会导致 runtime 的数据布局和 Go 里不一致,也是要考虑的问题。

package reflect

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

我们看 reflect 包的实现,其实他就是强转成 eface,从而获取到 runtime 里 _type 的数据。如果写惯 C/C++ 的朋友对这种代码应该不陌生,其实我们类型(对象)的本质是我们程序里的以我们知道的方式去读取内存,指针只是一个内存地址,对于一段内存我们可以通过任意方式去读取(强制转换)。

 如这上图,同一段数据使用不同类型的指针去获取能有不一样的结果。而 Go 里的反射,其实就是加了类型检查的指针数据获取。

这里有个问题,Go 不是没有对象头吗?怎么检查的结果。是的,Go 没有对象头,他对数据的检查是在编译阶段,他是强类型的,除非你使用 unsafe.Pointer。刚所说的检查类型发生在 interface{} 进行转换时,比如我们熟悉的 a.(b) 这种场景,而反射过程在 reflect 包里也会通过 eface 拿到 _type 然后进行类型断言。

ValueOf

reflect.ValueOfreflect.TypeOf 很类似,也是通过 eface 转换来取得类型和值的指针。

这里有个特殊的处理,所有传入 ValueOf 的值都会逃逸到堆上,因为 map 和 chan 的生命周期处理比较复杂,所以全部放到堆上比较好处理。原话:

Maybe allow contents of a Value to live on the stack. For now we make the contents always escape to the heap. It makes life easier in a few places (see chanrecv/mapassign comment below).

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值