golang method receiver-type的梗

分享:  

这里来聊聊method receiver type为什么不能是pointer和interface类型。

1 receiver-type必须满足的条件

golang里面提供了一定的面向对象支持,比如我们可以为类型T或者*T定义成员方法(类型T称为成员方法的receiver-type),但是这里的类型T必须满足如下几个条件:

  • T必须是已经定义过的类型;
  • T与当前方法定义必须在同一个package下面;
  • T不能是指针;
  • T不能是接口类型;

前面两点都比较容易理解,下面两点是什么梗?为什么就不能在指针类型上添加方法?为什么就不能在interface上添加方法?当然可以一句话待过,golang不支持,但是我想问下为什么?

2 receiver-type为什么不能是指针类型?

golang允许为 类型指针*T 添加方法,但是不允许为 指针类型本身 添加方法。按现有golang的实现方式,为指针类型添加方法会导致方法调用时的歧义

看下面这个示例程序。

type T int 
func (t *T) Get() T { 
    return *t + 1 
} 
type P *T 
func (p P) Get() T { 
    return *p + 2 
} 
func F() { 
    var v1 T 
    var v2 = &v1 
    var v3 P = &v1 
    fmt.Println(v1.Get(), v2.Get(), v3.Get()) 
}

示例程序中 v3.Get() 存在调用歧义,编译器不知道该调用哪个方法了。如果要支持在指针这种receiver-type上定义方法,golang编译器势必要实现地更复杂才能支持到,指针本来就比较容易破坏可读性,还要在一种指针类型上定义方法,对使用者、编译器开发者而言可能都是件费力不讨好的事情。

3 receiver-type为什么不能是接口类型?

这没有什么好揣测的,只是golang runtime不支持而已。golang现在的实现里,interface内部结构只能表示方法原型,但是不包括方法定义,struct才可以包括方法定义(这部分内容感兴趣的可以翻下golang的源码,这里不展开了)。

当一个类型实现了某个接口声明的全部方法时,就说这个类型实现了这个接口,就可以将这个类型的值赋值给该接口类型的值,此时会更新接口值的 dynamic value和dynamic type 字段。这里需要 根据接口中是否定义了方法列表 来进一步分析,可以细分为 接口方法列表为空接口方法列表不空 两种情况讨论。

下面以这里的示例代码为例进行分析:

// - 接口类型1
type EmptyIface interface{
}
// - 接口类型2
type Stringer interface {
    String() string
}

// - 类型Binary实现了接口Stringer,也实现了EmptyIface
type Binary uint64

func (i Binary) String() string {
    return strconv.Uitob64(i.Get(), 2)
}

func (i Binary) Get() uint64 {
    return uint64(i)
}

// - 为接口值赋值
var b Binary = Binary(1)
var eface EmptyIface = b
var iface Stringer = b
fmt.Println(iface)

3.1 空接口interface

eface对应接口类型是interface{},并且Binary值b小于等于sizeof(uintptr),那么eface的dynamic value就是直接拷贝b;如果这里是eface := anyBigStruct {….},并且anyBigStruct size大于sizeof(uintptr),那么就需要开辟内存拷贝anyBigStruct的值,并将新开辟内存的地址设置到dynamic value字段。dynamic type就指向对应类型的定义了。

此时的接口类型可以表示为:

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

此时的接口值可以表示为: eface

3.2 非空接口interface{ method-list }

如果iface对应接口类型interface{methods-list},这个时候dynamic value还是与1)中同样的处理,但是dynamic type处理方式不同,需要对运行时接口方法的调用地址进行处理

这时会创建一个 itable(类似c++虚函数vtable),这个itable里面保存了iface的接口类型以及接口中声明的各个方法原型对应的调用地址,调用地址从何而来?这里的调用地址也就是接口值动态类型中实现的方法的调用地址。设置完接口方法的所有调用地址后,itable构建完成,然后将iface接口值里面的tab字段指向该itable。

此时的接口类型可以表示为:

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

此时的接口值可以表示为: iface

3.3 接口方法调用

通过接口值调用方法的时候,需要根据 “该接口值类型、接口值对应的动态类型” 查询对应的接口方法实际的调用地址。如果该接口类型里面根本就没有方法列表,那肯定就报错了也不会查询tab,eface里面也没这个字段;如果接口类型里面有这个方法定义,那就根据iface.tab指向的itable去查询对应该接口类型、动态类型的对应方法的调用地址,然后执行目标地址处的方法体。

从将Binary值b赋值给iface,再到通过iface查询动态类型Binary中实现的方法Stringer调用地址,最终执行Binary中实现的方法String(),这整个逻辑处理过程中如何对接口类型声明方法列表、动态类型实现方法列表进行处理,我们应该清楚了。

总而言之,现在golang实现中interface的内部表示不包含“方法定义”相关的字段,无法表示为interface添加的方法定义,编译器当然也不允许。

不同的编程语言对函数调用的多态实现有不同的思路,c++是通过填充基本类型的虚函数表的方式,golang是通过类似的这样一种查询表的形式。总结一下就是并非golang无法通过扩展支持在接口上添加方法,只是这样是否真的有必要,值得商榷。至少非空接口是用来定义一种明确的行为(contract)的,在上面又加个方法,是什么意思呢?因此不允许将接口类型定义为receiver type也是与接口的定位相对应的吧。

4 总结

receiver-type不允许为指针类型和接口类型,对其中原因进行了一点思考和总结。

参考资料

  1. Russ Cox, “接口与itable关系”, https://research.swtch.com/interfaces

参考图片