• Go 反射的一些思考


    什么是反射

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

    1. func marshal(v any) {
    2. reflectValue(reflect.ValueOf())
    3. }
    4. func reflectValue(v reflect.Value) {
    5. valueEncoder(v)()
    6. }
    7. func valueEncoder(v reflect.Value) encoderFunc {
    8. return typeEncoder(v.Type())
    9. }
    10. func typeEncoder(t reflect.Type) encoderFunc {
    11. f, loaded = encoderCache.LoadOrStore()
    12. if loaded {
    13. return f
    14. }
    15. f = newTypeEncoder(t)
    16. encoderCache.Store()
    17. return f
    18. }
    19. func newTypeEncoder(t reflect.Type) encoderFunc {
    20. switch t.Type() {
    21. case reflect.Bool:
    22. ...
    23. case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
    24. ...
    25. case reflect.Struct:
    26. ...
    27. ...
    28. }
    29. }

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

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

    反射是怎么做的

    TypeOf

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

    1. type MyInterface interface {
    2. Hello(word any)
    3. }
    4. type MyStruct {
    5. }
    6. func (MyStruct) Hello(word any) {
    7. fmt.Println(word)
    8. }
    9. func mian() {
    10. var a MyInterface = &MyStruct{}
    11. b := 1
    12. a.Hello(b)
    13. }

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

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

    1. type eface struct {
    2. _type *_type
    3. data unsafe.Pointer
    4. }

    _type 的结构是:

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

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

    1. package reflect
    2. func TypeOf(i any) Type {
    3. eface := *(*emptyInterface)(unsafe.Pointer(&i))
    4. return toType(eface.typ)
    5. }

    我们看 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).

  • 相关阅读:
    FBV与CBV, CBV源码剖析
    Linux基础(三)----->权限理解和文件属性
    Android好看的动画欢迎界面(附有详细代码)
    C++ 20语言特性
    审计日志功能实现优化及测试记录(参照若依系统,以dolphinscheduler 2.0.5 为例,实现相关功能)
    SPARKSQL3.0-PhysicalPlan物理阶段源码剖析
    Ansible-Playbook 剧本语法
    如何消除左递归、何时需要消除左递归—编译原理
    【RPA小知识】RPA和AI的三大区别讲解
    FFmpeg--音频解码流程:aac解码pcm
  • 原文地址:https://blog.csdn.net/picone/article/details/126337031