GoLang之包装方法系列一

GoLang之包装方法系列一

1.问题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.问题2:为什么要生成包装方法?

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

如果是为了支持通过指针直接调用值接收者方法,那么直接在调用端进行指针解引用就可以了,总不至于为此生成包装方法吧?
为了验证这个问题,笔者又写了个函数用来反编译:
实验:包装方法是否为了支持通过指针直接调用值接收者方法

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()实际上会在调用端对指针解引用,然后调用值接收者方法(本质上就是编译器提供的语法糖),并没有调用编译器生成的包装方法。那这个包装方法究竟有什么用途呢?

2.2真正的原因

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

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

对于接口来讲,在调用指针接收者方法时,传递地址是非常方便的,也不用关心数据的具体类型,地址的大小总是一致的。
假如通过接口调用值接收者方法,就需要通过接口中的data指针把数据的值拷贝到栈上,由于编译阶段不能确定接口背后的具体类型,所以编译器不能生成相关的指令来完成拷贝,所以说,接口是不能直接使用值接收者方法的,这就是编译器生成包装方法的根本原因。

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

还记得介绍defer相关内容时讲到的runtime.reflectcall函数吗?它能够在运行阶段动态的拷贝参数并完成函数调用。
如果基于reflectcall的话,能不能实现通过接口调用值接收者方法呢?肯定是可以实现的,接口的itab中有具体类型的元数据,确实能够应用reflectcall。但是有个明显的问题:性能太差。跟几条用于传参的MOV指令加一条普通的CALL指令相比,reflectcall的开销太大了,所以Go语言选择为值接收者方法生成包装方法。
但是如果反编译或者用nm命令来分析可执行文件的话,就会发现:不只是这些包装方法,就连代码中的原始方法也不一定会存在于可执行文件中。这是怎么回事呢?

拓展问题:
为什么并不是所有包装方法都存在于可执行文件中
(未完待续~)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GoGo在努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值