研究!rsc公司

关于编程的想法和链接,通过

RSS(RSS)

Go数据结构:接口
发布于2009年12月1日星期二。

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的函数指针,为调用该函数做准备。

(评论最初通过Blogger发布。)

  • 罗格·佩佩 (2009年12月2日2:42 AM) Go的界面允许您像在Python这样的纯动态语言中一样使用duck类型

    这句话已经说了很多,但我不认为是真的。鸭子打字更深入。例如,如果我有一个做算术的API:


    类型Foo结构{…};

    func(f0*Foo)加法(f1*Foo)*Foo;
    func(f0*Foo)Negate()*Foo;



    对于duck类型,这与响应相同方法的任何其他类型都兼容,在其方法返回的对象上包含方法。如果接口真的像鸭子类型,那么上面的结构将与这个接口兼容:


    类型Arith接口{
    添加(f1 Arith)Arith;
    否定()Arith;
    }


    但事实并非如此。

  • 弗雷德·布拉斯德尔 (2009年12月2日凌晨3:26)这比rog更糟糕:Go语言本身没有使用它提供给用户的多态性机制(接口),它使用的多态性(数组和映射的参数)只是为了它的使用。

    可以理解,他们害怕超载,而古典主义的反革命可能会扰乱语言的公共发展(参见Prototype.js,了解一个特别讽刺和令人沮丧的例子)。他们必须小心添加的内容。


    我不知道围棋是否是第一种使用这种技术的语言,但它肯定不是一种常见的语言。

    我确信这在卡德利和阿巴迪的《物体理论》中已经讨论过了,但我没有自己的副本。我打赌Rob有一个你可以借的:)

  • 卡拉尼 (2009年12月2日上午6:41)弗雷德,我不知道为什么这里会有过载的恐惧(尽管你已经指出了这种语言的一些主要问题)。特别是对于糟糕的String/toString示例,应该有人立即看到了引入重载的必要性。希望有更好的理由接受运行时类型标记检查带来的不必要的时间/空间开销(更重要的是逻辑/推理开销)。

    很遗憾,Go的作者应该读过Mark Jones关于限定类型的论文。如果没有多态递归,它们可以静态地为基元类型构造所有必要的“接口”,并具有更好的“toString”函数(更不用说还有一百万个其他示例)。

    这些人错过了一个认真推进“系统编程”状态的好机会

  • 埃文·琼斯 (2009年12月10日下午4:31)主题:方法查找性能。在我看来,为Method查找性能显示的示例不太“公平”。我认为,对于Javascript和Python,问题是如果代码更改类型或实例,对s.String()的调用可能会在每个循环迭代中调用不同的方法。

    围棋有点静态,我认为这是不可能的。如果Python/Javascript禁止更改对象上的方法,那么优化器可以将方法查找识别为常量,并将其从循环中取消。

    但我没有认真考虑过这个问题,所以我可能错了。

  • 汤姆 (2010年7月18日上午8:57) 我不知道Go是否是第一种使用这种技术的语言,但它肯定不是一种常见的语言。

    Haskell TypeClasses使用类似的技术实现——实现类型类的每个类型(或类型集)都与一个方法表(称为词典)传递给需要所述类型类的值的函数。

  • 乡绅 (2010年10月14日上午9:45)
    我不知道Go是否是第一种使用这种技术的语言,但它肯定不是一种常见的语言。


    Emerald动态计算查找表;我们称它们为“AbCon向量”,因为它们从编译时已知的抽象类型(aka接口)转换为对象的具体类型(aka-class),有时只有在运行时才知道。我们还缓存了AbCons。

    Emerald也或多或少地进行了类型检查,正如你在Go中所描述的那样,接口是结构化的(程序员不必声称他们实现了一个接口,他们只需要这样做),尽可能检查是静态的,否则检查是动态的,并允许运行时类型查询和检查运行时转换。Emerald也有多个返回值和“逗号赋值”的方法。

    你确定你没有偷看吗?

  • 俄罗斯考克斯 (2010年10月14日下午7:46)@乡绅:

    真 的。

    2007年关于祖母绿的回顾性论文我已经完成了一半,我的博客帖子图片也可能是从图3中删除的。

    我确信我们没有偷看,但我认为有趣的是,团队成员的背景如此相似。在Go的案例中,Rob通过Hoare的CSP带来了并发思想,Robert带来了Smalltalk程序员的对象敏感性,Ken关注C质量性能,这导致了Emerald在25年前使用的基本相同的数据结构。这种描述显然过于简单化了,但你可以在论文对原始翡翠团队的描述中看到相同的背景。

    很酷的是,不仅看到其他人多年前探索过这个空间,而且他们的设计基本上是一样的。事实上,这个设计点已经被独立团体多次发现,这让它看起来更为基本。

    非常感谢您指出这一点。发现这些联系真是令人兴奋。

  • 德鲁·勒苏厄 (2010年12月5日2:42 AM)非常感谢你的指导。你最后的代码真的帮助我理解了一些围棋基础知识

  • 匿名(2011年6月5日下午7:43)“很酷”——一点也不酷;这表明对文献的搜索不足,这在围棋的整个设计中都可以看到。经验丰富的语言设计师看着Go,只是摇了摇头。但这是罗伯·派克和肯·汤普森!是的,正是——一群聪明、固执己见的怪人,他们的独特想法,而不是对语言设计的深入了解,推动了围棋的设计。我们可以在Perl和Java中看到相同的东西——相反方向的特性。然后是D,它是一个火车残骸,因为它的作者把它当作一个个人项目。谷歌本可以雇佣沃尔特·布莱特,并将D变成C++的更好替代品,因为它在一开始就有潜力成为C++的替代品——尽管他们本可以通过雇佣马丁·奥德斯基这样的人来做得更好。

  • 果酱 (2011年7月31日上午7:33)我注意到缺少的一个技巧是:
    stringr=s.字符串
    对于我。。。
    打印纵梁()

    即能够在循环外进行属性查找。我经常使用它,尤其是在理解方面。

    对于go来说,它仍然可以帮助避免额外的间接寻址。但go似乎不允许绑定函数和使用:
    F=func()字符串{return s.string()}

    事实上,情况变得更糟了。

  • 右侧咨询室 (2011年12月11日上午9:56)Este blogéuma代表了竞争环境。Eu gosto da sua recomendaço。嗯,伟大的conceito que reflete os pensamentos做代言人。右侧咨询室