Go数据结构:接口
Go的界面——静态、编译时检查、请求时动态、,对我来说,从语言设计的角度来看,围棋最令人兴奋的部分。如果我能将Go的一个特性导出到其他语言中,那就是接口。
本文是我对“gc”编译器:6g、8g和5g。在艾尔斯,伊恩·兰斯·泰勒写道二
帖子关于中接口值的实现gccgo公司
.实现方式与其说不同,不如说相似:最大的区别是这篇文章有图片。
在查看实现之前,让我们先了解一下它必须支持什么。
用法
Go的接口让您可以使用鸭子打字就像你一样在纯动态语言(如Python)中,但仍具有编译器会捕获明显的错误,如传递整数
其中一个对象用一个阅读
应为方法,或比如打电话给阅读
方法的参数数目错误。要使用接口,首先定义接口类型(例如,读得更近
):
类型ReadCloser接口{读取(b[]字节)(n int,err os.Error)关闭()}
然后将新函数定义为读得更近
.例如,此函数调用阅读
反复获得所有请求的数据,然后调用关闭
:
func ReadAndClose(r ReadCloser,buf[]字节)(n int,err os.Error){对于len(buf)>0&&err==nil{变量nr intnr,err=r.Read(buf)n+=数量buf=buf[编号:]}r.关闭()返回}
调用的代码读取并关闭
可以通过任何类型的值,只要它有阅读
和关闭
方法有正确的签名。而且,与Python之类的语言不同,如果传递一个值错误的类型,将在编译时出错,而不是在运行时。
不过,接口并不局限于静态检查。您可以动态检查特定接口值有一个附加方法。例如:
类型Stringer接口{String()字符串}func ToString(任何接口{})字符串{如果v,确定:=任何。(斯特林格);好的{return v.String()}开关v:=任意。(类型){案例int:返回strconv。伊托阿(v)案例浮动:返回strconv。Ftoa(v,‘g’,-1)}返回“???”}
价值观任何
具有静态类型接口{}
,意味着不保证任何方法:它可以包含任何类型。中的“逗号ok”赋值如果
语句询问是否可以转换任何
到类型的接口值纵梁
,其中包含方法字符串
。如果是,则为该声明的正文调用该方法以获取要返回的字符串。否则转换
之前选择了一些基本类型放弃。这基本上是对fmt包做。(如果
可以通过添加箱子纵梁:
位于转换
,但我用了一个单独的声明来提醒大家注意这张支票。)
作为一个简单的示例,让我们考虑一个64位整数类型用一个字符串
打印值的方法二进制和平凡获取
方法:
类型Binary uint64func(i二进制)String()字符串{返回strconv。Uitob64(i.Get(),2)}func(i二进制)Get()uint64{返回uint64(i)}
类型为的值二元的
可以通过到ToString(目标字符串)
,它将使用字符串
方法,尽管程序从未这样说二元的
打算实施纵梁
.没有必要:运行时可以看到二元的
有一个字符串
方法,因此它实现纵梁
,即使作者二元的
从未听说过纵梁
.
这些示例表明,尽管隐式转换在编译时检查,显式接口到接口转换可以在运行时查询方法集。“有效Go”提供了有关如何使用接口值的更多详细信息和示例。
接口值
带有方法的语言通常属于两个阵营:静态地为所有方法调用准备表(如C++和Java),或在每次调用时进行方法查找(就像Smalltalk及其许多模仿者一样,包括JavaScript和Python)并添加高级缓存以提高调用效率。Go位于两者之间:它有方法表但在运行时计算。我不知道围棋是否是第一种使用这种技术的语言,但这肯定不是常见的。(我很想听听前面的例子;请在下面留言。)
作为预热,类型为的值二元的
只是一个64位整数由两个32位单词组成(如最后一个帖子,我们假设一台32位机器;这个时间记忆向下生长而不是向右):
接口值表示为两个单词对,给出指向存储在接口中的类型信息的指针以及指向关联数据的指针。分配b条
到类型的接口值纵梁
设置接口值的两个单词。
(接口值中包含的指针为灰色为了强调它们是隐含的,不直接公开给Go程序。)
接口值中的第一个单词指向我称之为接口表或itable(发音为I-table;在运行时源,C实现名称为伊塔布
).itable以关于类型的一些元数据开始然后成为函数指针列表。注意,itable对应于接口类型,而不是动态类型。就我们的例子而言对于纵梁
保持类型二元的
列出了用于满足纵梁
,这只是字符串
:二元的
的其他方法(获取
)不露面在表格中。
接口值中的第二个单词指向在实际数据中,在本例中是b条
.任务var s纵梁=b
制造的副本b条
而不是指向b条
因为同样的原因变量c uint64=b
制作副本:如果b条
以后的更改,秒
和c(c)
应该是要有原始值,而不是新值。接口中存储的值可能任意大,但只有一个词专门用于保持价值在接口结构中,因此赋值分配内存块并将指针记录在单字槽中。(当值合适时,会有明显的优化插槽中;我们稍后再讨论。)
要检查接口值是否包含特定类型,如类型开关以上,Go编译器生成与C表达式s.tab->类型
以获得类型指针并根据所需类型进行检查。如果类型匹配,则可以通过复制值取消引用美国数据
.
拨打电话s.字符串()
,Go编译器生成执行等效于C表达式的代码s.tab->乐趣[0](s.data)
:它调用适当的来自itable的函数指针,传递接口值的数据字作为函数的第一个参数(在本例中是唯一的)。如果您运行8克-S x.go
(详情见本帖底部)。注意,itable中的函数被传递给接口值第二个字的32位指针,而不是64位它所指向的值。通常,接口调用站点不知道这个词的意思以及它指向的数据量。相反,接口代码安排函数itable中的指针需要32位表示存储在接口值中。因此,本例中的函数指针为(*二进制)。字符串
不二元的。字符串
.
我们考虑的示例是一个接口只有一种方法。具有更多方法的接口将中有更多条目乐趣底部的列表适合的。
计算Itable
现在我们知道了衣料是什么样子,但在哪里他们来自哪里?Go的动态类型转换意味着编译器或链接器预计算所有内容是不合理的可能的问题:(接口类型、具体类型)对太多,大多数都不需要。相反,编译器为每个混凝土类型二元的
或整数
或func(映射[int]string)
.在其他元数据中,类型描述结构包含一个列表该类型实现的方法。类似地,编译器生成(不同的)类型描述结构对于每个接口类型,如纵梁
; 它也包含一个方法列表。接口运行时通过查找列出的每个方法来计算itable在接口类型的方法表中具体类型的方法表。运行时在生成itable后缓存它,因此,此对应关系只需计算一次。
在我们的简单示例中纵梁
有一个方法,而二元的
有两种方法。一般来说镍方法对于接口类型和纳特具体类型的方法。从界面查找映射的明显搜索从方法到具体方法O(运行)(镍×纳特)时间,但我们可以做得更好。通过对两个方法表进行排序并遍历它们同时,我们可以构建映射在里面O(运行)(镍+纳特)而是时间。
内存优化
使用的空间上述实现可以通过两种互补的方式进行优化。
首先,如果涉及的接口类型是空的——它没有方法——那么除了保持指向原始类型。在这种情况下,可以删除itable并该值可以直接指向类型:
接口类型是否具有方法是一个静态属性,源代码中的类型为接口{}
或者上面说交互{方法…}
-所以编译器知道在每个点上使用的是哪种表示在程序中。
其次,如果与接口值关联的值可以放入一个机器字,无需介绍间接寻址或堆分配。如果我们定义二进制32
就像二元的
但实现为单位32
,它可以存储在接口值中通过在第二个单词中保留实际值:
实际值是指向还是内联取决于类型的大小。编译器安排类型方法表中列出的函数(被复制到衣箱中)做正确的事情传入的单词。如果接收器类型适合一个单词,直接使用;如果没有,则取消引用。图表显示:在二元的
版本远高于此,表中的方法是(*二进制)。字符串
,在二进制32
示例,方法在表中是二进制32.字符串
不(*二进制32)。字符串
.
当然,空接口包含单词大小(或更小)的值可以利用这两种优化:
方法查找性能
Smalltalk及其之后的许多动态系统每次调用方法时都执行方法查找。为了提高速度,许多实现都使用简单的单条目缓存在每个调用位置,通常在指令流本身中。在多线程程序中,必须管理这些缓存小心,因为多个线程可能处于同一调用中现场同时进行。即使比赛已经结束如果被避免,这些缓存最终将成为内存争用。
因为Go有静态类型的提示与动态方法查找一起,它可以移动查找从调用位置返回到存储值的点在界面中。例如,考虑以下代码片段:
1 var任何接口{}//在别处初始化2秒:=任意。(纵梁)//动态转换i:=0时为3;i<100;i++(i++){4英尺。Println(s.String())5 }
在Go中,可以计算(或在缓存中找到)在第2行的作业期间;为s.字符串()
在第4行执行的调用是两次内存获取和一次间接调用说明。
相反,此程序在动态类似Smalltalk(或JavaScript,或Python,或…)的语言将在第4行执行方法查找,该行在循环中重复不必要的工作。前面提到的缓存使其成本更低可能是,但它仍然比一次间接呼叫更贵说明。
当然,这是一篇博客帖子,我没有数字支持了这一讨论,但它看起来确实像缺少内存争用将是高度并行的程序,就像能够移动方法一样查找紧密循环。还有,我说的是将军架构,而不是实现的细节:后者可能还有一些常量因子优化可用。
更多信息
接口运行时支持位于$GOROOT/src/pkg/runtime/iface。c(c)
.关于接口还有很多要说的(我们还没有甚至还看到了指针接收器的示例)和类型描述符(它们还支持反射接口运行时),但这些都必须等待将来的发布。
代码
支持代码(x.go(去)
):
成套设备总管导入(“柔性制造技术”“strconv”)类型Stringer接口{String()字符串}类型Binary uint64func(i二进制)String()字符串{返回strconv。Uitob64(i.Get(),2)}func(i二进制)Get()uint64{返回uint64(i)}函数main(){b:=二进制(200)s:=纵梁(b)fmt公司。Println(字符串())}
选定的输出8克-S x.go
:
0045(x.go:25)LEAL s+-24(SP),BX0046(x.go:25)MOVL 4(BX),英国石油公司0047(x.go:25)移动BP,(SP)0048(x.go:25)移动(BX),BX0049(x.go:25)MOVL 20(BX),BX0050(x.go:25)呼叫,BX
这个LEAL公司
加载的地址秒
进入寄存器BX公司
.(注释n个(SP)
描述了内存中的单词特殊用途+n个
.0(SP)
可以缩短为(SP)
.)接下来的两个MOVL(移动)
指令获取接口和存储中第二个单词的值它作为第一个函数调用参数,0(SP)
.最后两个MOVL(移动)
指令获取itable,然后是来自itable的函数指针,为调用该函数做准备。