首先要明确一点,程序的本质是代码 + 数据,我们编写代码的本质是为了控制数据、处理数据。在现代编程中,我们常常会有动态的东西,运行时才知道操作的数据是什么,无法编译时候确定,就需要反射。比如最常用的 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。
总的来说,反射就是运行时获取到数据的结构,并加以操作。很多无法编译时确定的逻辑都可以使用反射来实现。
首先要理解 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 然后进行类型断言。
reflect.ValueOf
和 reflect.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).