golang interface的使用和实现(翻译整理)


 
一、使用
Go的interface可以让你使用像Python一样纯动态语言的duck typing,但是仍然可以在编译时检测到类型错误,
例如传递一个int参数到一个对象的Read方法,该方法期待int参数,或者使用错误的参数个数调用Read方法。
为了使用interface,首先定义一个interface类型。(比如,ReadCloser):

type ReadCloser interface {
Read(b []byte) (n int, err os.Error)
Close()
}

然后定义一个新的函数,参数带有ReadCloser。
例如,这个函数重复调用Read方法去获取所有请求的数据,然后调用Close:

func ReadAndCloser (r ReadCloser, buf []byte) (n int, err os.Error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:]
}
r.Close()
return
}

调用ReadAndClose的代码可以传递任何实现了正确函数签名的Read和Close方法 的类型。
并且,不像Python,如果你传递了错误的类型,可以在编译时检测到,而不是运行时。
interface不限于静态检查。可以动态的检查特定接口值是否具有其他方法。例如:

type Stringer interface {
String() string
}
func ToString(any interface{}) string {
if v, ok := any.(Stringer); ok {
return v.String()
}
switch v := any.(type) {
case int:
return strconv.Itoa(v)
case float:
return strconv.Ftoa(v, 'g', -1)
}
return "???"
}

any值有个静态的类型interface{},意味着不保证有任何方法:可以包含任何类型。
在if语句中的 v, ok = any.(Stringer); 赋值语句询问是否可以将any变量转换为Stringer类型(有String方法)。
(这里any查看any.tab->type 是否 是Stringer 类型,如果是,返回any.data)
如果是,该语句的正文将调用该方法来获取要返回的字符串。
否则,在放弃前,使用switch语句检查是否是其他类型可以转换为字符串来返回。

这基本上是fmt package的简单版本。
(if语句可以在switch中增加case Stringer来代替:在switch body的第一个case,但是我使用了一个单独的声明来强调)
作为一个简单的例子,考虑一个64位的integer类型,使用String和Get方法打印二进制值:

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

Binary类型的值可以传递给ToString,ToString将会使用String方法格式化,即使程序从未说明Binary将会实现Stringer。
没必要那样做,因为:运行时可以看到Binary有String方法,因此它实现了Stringer,即使Binary的作者从未听说过Stringer。
这些例子展示了,即使在编译时检查了所有的隐式转换,显示的interface-to-interface转换也可以在运行时查询方法集。


二、interface的value值

 有method的语言通常在两个阵营之一:为所有method调用准备一个table(C++和Java),或为每个method调用执行method lookup(包括Smalltalk和它的拥趸,JavaScript和Python)并且使用缓存来使调用更有效率。Go是两种方式的折中:它有method table,但是在运行时计算。不知道是否Go语言是第一个使用这个技术的语言,但肯定不常见。

作为热身,Binary类型的值是一个64位的integer,由两个32位的word组成(假设是32位的机器):

        

interface的值相当于两个word,保存了两个指针,一个指向存储在interface中的类型信息,另一个指向相关的数据。
将b赋值给类型为Stringer的interface值,将会设置interface value的两个word值。

     

(上图的指针线是灰色的,强调他们是隐式的,不会直接暴露给Go程序)

第一个word指向一个interface table或itable(发音为i-table;C语言实现名为Itab)。
itable以涉及类型的元数据开始,后面是一系列函数指针。
注意,itable对应于接口类型,不是动态类型。
在我们的例子中,Stringer 的itable hold Binary类型,列出了用于satisfy Stringer的method,即String。
Binary的其他method(GET),不会出现在itable表中。
第二个word值指向实际的数据,在这里是b的一个副本。
var s Stringer = b 语句拷贝了b的副本,而不是指向b,同样,var c uint64 = b 同样拷贝副本,如果b改变了值,c和s将还是原值。由于数据可能很大,但是interface结构中,只有一个word可用,因此在栈上分配一块内存,在这个word上记录这块内存的地址。(当数据用一个word可以容纳时,显然可以优化这种方式,可以用这个word直接存储)
检查interface值是否hold一个特定的类型(如上面的type switch),Go编译器生成代码 :s.tab->type 获取类型指针,检查是否是想要的类型。 如果类型匹配,可以通过 s.data 拷贝数据(interface中保存的类型原始数据) (v, ok := i.(T)做的事)
为了调用 s.String(),Go编译器生成代码: s.tab->fun[0](s.data):从itable中调用适当的函数指针,传递interface的data word指针作为该函数的第一个参数。注意itable中的函数,传递给它的参数是interface value值的第二个32位word值,而不是其指向的64位的值。通常,interface的调用方,不知道这个word的意义,也不知道该word指向的数据的容量。相反,interface code 安排 itable中的函数指针并期待存储在interface中的32位值。因此这个例子中的函数指针是(*Binary).String 而不是Binary.String。
上面的例子只考虑一个method的情况。多个method的话,interface在itable底部有多个函数列表的条目。


三、计算生成Itable(compuing the Itable)

我们看到了itables是什么样的,但是他们从哪里来的?Go的动态类型转换,意味着compiler或linker 预先计算所有可能的itable是不合理的:
因为有太多的对(接口类型,实体类型),大多数都不需要。相反,compiler为每个实体类型,例如Binary、int、func(map[int]string),生成一个type描述结构。除了其他元数据, type描述结构(Type *)包含一系列该type实现的method。
类似地,compiler也为每个interface类型,例如Stringer,生成一个 type描述结构(InterfaceTpye*)。也包含一组method。interface在运行时( 早期go版本是在运行时检测,1.8.3版本是在编译时检测),通过寻找在InterfaceType中和在实体类型中UncommonType字段指向的method来 生成一个itable结构体具体类型转换成空接口,这个过程比较简单,就是返回一个Eface,将Eface中的data指针指向原型数据,type指针会指向数据的Type结构;具体类型转换为带方法的接口类型是在编译过程中进行检测的,当将某个类型数据转换为带方法的interface时,会复杂一些,中间涉及一道检测,该实体类型必须要实现接口中声明的所有方法才可以进行转换)。最后将Type方法表中的函数指针(函数实现),拷贝到Itable的fun字段中( 即:将Type函数的实现拷贝到Itable的fun字段)。

像,v, ok := i.(T) 这样的语法,也是判断一个interface i的具体类型是否为类型T,如果是则将其data返回给v。
这也会检测转换是否合法,不过这里的检测是在运行时执行的。在runtime下的iface.c文件中,有一系列的assetX2X函数,比如runtime.assetE2T,runtime.assetl2T等等。这个实现起来比较简单,只需要比较Iface中的Itab的type是否与给定Type为同一个。
在我们的例子中,Stringer的method表只有一个方法,而Binary的方法表有两个method。
通常,interface类型可能有ni个method,实体类型有nt个method。很明显interface method到实体类型method的映射有复杂度为 ni*nt,但是我们可以做的更好。通过排序两个method表,同步遍历,我们可以将复杂度降低到 ni + nt。


四、内存优化

上述实现使用的内存空间,可以通过两个方面来优化。
第一,如果是一个空的interface type - 它没有method - itable除了指向原type没有其他的作用。在这种情况下,itable可以丢弃,并且可以直接指向type:

是否interface type有method是一个静态属性 - 代码形式为 interface {} 或 interface { methods ... } - 因此 编译器知道在程序中用哪种形式。
第二,如果interface的data可以用一个word容纳,没有必要引入间接引用或者分配堆栈。
如果我们像Binary定义了一个Binary32,但是用uint32实现,这个值可以直接存储在interface value的第二个word中:

实际的值是被指向的还是内嵌的,取决于这个类型的size。编译器为type类型中的method表中的函数(要拷贝到itable中的method)处理如何传入一个word字段。如果receiver type fits一个word,直接使用这个word。如果不是,间接引用。上图展示:上面上面的Binary版本,itable中的method是(*Binary).String,但是Binary32版本,itable中的method是Binary32.String,而不是(*Binary32).String。
当然,空的interface 值是word(或者更小),可以享受到两种优化:



五、Method Lookup Performance

当一个method被调用时,Smalltalk和许多它的动态系统后继者,每次都会执行method查询。
为了追求速度,许多实现在每个call site使用一个简单的one-entry cache,经常用于指令流本身。在一个多线程的程序中,这些cache必须小心的管理,因为多个线程可能同时在相同的call site。即使避免了竞争,cache最终成为内存争用的资源。
因为Go具有动态方法查找加静态类型提示,可以将查找从调用点移到存储在interface中的值。

1 var any interface{}
2 s := any.(String) // 动态转换
3 for i := 0; i < 100; i++ {
4 fmt.Println(s.String())
5 }
在Go中,itable在第二行计算生成(或者在cache中查找到的),第4行执行的s.String()是一组内存读取和一个间接调用指令。
相反,像Smalltalk(或javaScript,Python或其他)这样的动态语言执行这个程序时,将会在第四行执行method lookup,将会在循环中重复没必要的工作。先前提到的cache会使开销降低一些,但是仍然比一条间接调用指令开销大。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值