• 深度探索Go语言:包装方法


    问题1:什么是包装方法?

    下面咱们来验证下包装方法的存在:

    首先,定义一个Point类型,表示一维坐标系内的一个点,并且按照Go语言的风格为其实现了一个Get方法和一个Set方法。

    package gom
    ​
    type Point struct {
      x float64
    }
    ​
    func (p Point) X() float64 {
      return p.x
    }
    ​
    func (p *Point) SetX(x float64) {
      p.x = x
    }

    然后,采用只编译不链接的方式来得到OBJ文件,再对编译得到的OBJ文件进行反编译分析。编译命令如下:

    $ go tool compile -trimpath="`pwd`=>" -l -p gom point.go

    上述命令禁用了内联优化,编译完成后会在当前工作目录生成一个point.o文件,这就是我们想要的OBJ文件。

    接下来,通过go tool nm可以查看该文件中实现了哪些函数,nm会输出OBJ文件中定义或使用到的符号信息,通过grep命令过滤代码段符号对应的T标识,即可查看文件中实现的函数:

    $ go tool nm point.o | grep T
        1562 T gom.(*Point).SetX
        1899 T gom.(*Point).X
        1555 T gom.Point.X

    可以看到point.o中一共实现了3个方法,它们都定义在Point类型所在的gom包中:

    第一个是Point的SetX方法,它的接收者类型是*Point,第三个是Point的X方法,它的接收者类型是Point,这些都与源代码一致。

    比较奇怪的是第二个方法,这是一个接收者类型为*Point的X方法,源代码中并没有这个方法,它是怎么来的呢?只能是编译器生成的。

    编译器会为接收者为值类型的方法生成接收者为指针类型的方法,也就是所谓的“包装方法”。

    那么编译器为什么要生成它呢?


    问题2:为什么要生成包装方法?

    如果是为了支持通过指针直接调用值接收者方法,那么直接在调用端进行指针解引用就可以了,总不至于为此生成包装方法吧?

    为了验证这个问题,笔者又写了个函数用来反编译:

    实验:包装方法是否为了支持通过指针直接调用值接收者方法

    func PointX(p *Point) float64 {
      return p.X()
    }

    大致思路就是:通过指针来调用值接收者方法,再通过反编译看一下实际调用的是不是包装方法。反编译得到的汇编代码如下:

    $ go tool objdump -S -s '^gom.PointX$' point.o
    TEXT gom.PointX(SB) gofile..point.go
    func PointX(p *Point) float64 {
      0x1a17      65488b0c2528000000      MOVQ GS:0x28, CX
      0x1a20      488b8900000000          MOVQ 0(CX), CX          [3:7]R_TLS_LE
      0x1a27      483b6110                CMPQ 0x10(CX), SP
      0x1a2b      7637                    JBE 0x1a64
      0x1a2d      4883ec18                SUBQ $0x18, SP
      0x1a31      48896c2410              MOVQ BP, 0x10(SP)
      0x1a36      488d6c2410              LEAQ 0x10(SP), BP
            return p.X()
      0x1a3b      488b442420              MOVQ 0x20(SP), AX
      0x1a40      f20f1000                MOVSD_XMM 0(AX), X0
      0x1a44      f20f110424              MOVSD_XMM X0, 0(SP)
      0x1a49      e800000000              CALL 0x1a4e             [1:5]R_CALL:gom.Point.X
      0x1a4e      f20f10442408            MOVSD_XMM 0x8(SP), X0
      0x1a54      f20f11442428            MOVSD_XMM X0, 0x28(SP)
      0x1a5a      488b6c2410              MOVQ 0x10(SP), BP
      0x1a5f      4883c418                ADDQ $0x18, SP
      0x1a63      c3                      RET
    func PointX(p *Point) float64 {
      0x1a64      e800000000              CALL 0x1a69             [1:5]R_CALL:runtime.morestack_noctxt
      0x1a69      ebac                    JMP gom.PointX(SB)

    可以看到p.X()实际上会在调用端对指针解引用,然后调用值接收者方法(本质上就是编译器提供的语法糖),并没有调用编译器生成的包装方法。那这个包装方法究竟有什么用途呢?


    真正的原因

    之前我们已经介绍过接口的数据结构iface,它包含一个itab指针和一个data指针,data指针存储的就是数据的地址。

    type iface struct {
      tab  *itab
      data unsafe.Pointer
    }

    对于接口来讲,在调用指针接收者方法时,传递地址是非常方便的,也不用关心数据的具体类型,地址的大小总是一致的。

    假如通过接口调用值接收者方法,就需要通过接口中的data指针把数据的值拷贝到栈上,由于编译阶段不能确定接口背后的具体类型,所以编译器不能生成相关的指令来完成拷贝,所以说,接口是不能直接使用值接收者方法的,这就是编译器生成包装方法的根本原因。


    那么,就没有什么办法可以让接口间接使用值接收者方法吗?

    还记得介绍defer相关内容时讲到的runtime.reflectcall函数吗?它能够在运行阶段动态的拷贝参数并完成函数调用。

    如果基于reflectcall的话,能不能实现通过接口调用值接收者方法呢?肯定是可以实现的,接口的itab中有具体类型的元数据,确实能够应用reflectcall。但是有个明显的问题:性能太差。跟几条用于传参的MOV指令加一条普通的CALL指令相比,reflectcall的开销太大了,所以Go语言选择为值接收者方法生成包装方法。

    但是如果反编译或者用nm命令来分析可执行文件的话,就会发现:

    不只是这些包装方法,就连代码中的原始方法也不一定会存在于可执行文件中。

    这是怎么回事呢?(未完待续~)

  • 相关阅读:
    下定决心
    SPFA
    Linux下一些常用的命令
    如何设计符合RESTful风格的API
    django中的第三方:富文本编辑器和itsdangerous加密
    redis主从
    redis集群
    django中关联(一对多)查询的两种方式,理一理
    关于Django中的迁移文件
    日常工作中Git的正确使用姿势
  • 原文地址:https://www.cnblogs.com/gongxianjin/p/16254985.html
Copyright © 2020-2023  润新知