浅析Go语言的Interface机制

前几日一朋友在学GO,问了我一些interface机制的问题。试着解释发现自己也不是太清楚,所以今天下午特意查了资料和阅读GO的源码(基于go1.4),整理出了此文。如果有错误的地方还望指正。

GO语言的interface是我比较喜欢的特性之一。interface与struct之间可以相互转换,struct不需要像JAVA在源码中显示说明实现了某个接口,可以通过约定的形式,隐式的转换到interface,还可以在运行时查询接口类型,这样有种用动态语言写代码的感觉,但是又可以在编译时进行检查,捕捉一些明显的类型不匹配的错误。

type Stringer interface {
    String() string
}

type S struct {
     i int
}

func (s *S) String() string {
    return fmt.Sprintf("%d", s.i)
}

func Print(s Stringer) {
    println(s.String())
}

func DynamicPrint(any interface{}) {
   if s, ok := any.(Stringer); ok {
       Print(s)
   }
}

func main() {
   var s S
   s.i = 123456789
   Print(&s)
   DynamicPrint(&s)
}

如上面的代码所示,类型S没有显示的实现Stringer接口,但是它的方法列表符合Stringer接口,所以可以转换为Stringer接口使用。

那么,GO语言的interface机制到底是如何实现的呢?

interface value

上述代码中函数Print的参数是一个Stringer接口,也就是Stringer的一个对象实例。这个对象实例叫做interface value。它的数据结构如下:

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

其中tab字段类似于C++的vptr,tab中包含了对应的方法数组,除此之外还保存了实现该接口的类型元数据。data是对应的实现该接口的类型的实例指针。

itab数据结构如下:

type itab struct {
    inter     *interfacetype
    _type    *type
    link      *itab
    bad      int32
    unused  int32
    fun      [0]unsafe.Pointer
}

其中inter字段表示这个interface value所属的接口元信息,_type字段表示具体实现类型的元信息,fun字段表示该interface的方法数组。link,bad,unused字段暂时不关心。

当我们在GO代码中调用一个接口的方法时,操作类似如下: s.tab->fun[0](s.data)。调用开销还是很小的。

Itab的生成方式

一个自定义的结构体可以实现某个接口,然后可以隐式的转换到对应的接口。这种操作有点像C++的派生类转换为基类一样,这个操作是一个运行时绑定过程。而GO语言的interface机制还有一些其他特性:比如一个具体类型可以实现N多方法,但是只有其中某几个或者全部都满足某个接口,而此时,不可能把所有的方法都放到Itab中,这就意味着需要在绑定过程中剔除某些不需要的方法。

GO编译器会在编译时会为每个自定义结构体和interface类型生成一个类型元数据,用来描述这个类型的名称,类型的HASH值,类型的方法列表,方法列表中还包括了方法的名称。而在一个自定义结构体转换到一个interface类型时,GO编译器会生成代码,使其在运行时计算Itab,完成动态绑定方法的需求。这个计算Itab的过程相对来说比较简单,因为GO编译器生成的类型元数据中包含了所有的方法名称和地址,那么在一个结构体实例转换为interface value时,只需要把interface的方法列表作为基,方法名和方法类型作为KEY,去结构体元数据中查找对应的方法即可。

GO的runtime库中对Itab的查找过程做了优化,由O(ni * nt)复杂度变为O(ni + nt)。依据是一个自定义结构体实现的方法一定是大于或等于某个具体interface的方法集的。所以可以事先把所有的方法按照名字从小到大排序,然后在匹配到一个方法后,可以在下次查找时使用上次的索引值。

除此之外,GO编译器为了减少每次不必要的Itab,还增加了一个对应的itab的缓存。你可以编译一个GO程序,然后反编译后可以查看到一个类似go_itab__main_S_main_Stringer名称的变量。在每次一个结构体转换到一个interface之前都会检查这个缓存是否有效,有效就使用。这个检查也只是一个cmp指令而已。

还有在GO运行时库里,为了减少每次的Itab实现,还做了相应的优化。内部实现了一个HASH表,保存了每个具体结构体到interface转换生成的Itab实例。代码可以在go\src\runtime\iface.go getitab函数中看到。

interface{}的特殊处理

interface{}在GO中是一个特殊的内建类型,类似于C/C++中的void*,但是包含了类型信息。所以你可以把任意的数据转换到interface{},然后通过type assert从interface{}获取原有的数据。但是正如你所见,interface{}没有方法,那么也就是说,它不需要iface中的itab,因为不需要方法绑定。针对此,做了特殊修改,iface中的tab字段类型由itab指针变为了对应的具体实现类型的类型元数据指针。在GO源码中,interface{}对象的类型原型如下:

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

eface是empty interface的缩写。

其他  

在GO的源码iface.go中,还可以看到很多函数比如叫assertE2E,assertE2I,assertE2T等,这些函数就是对应的type assert的具体实现函数。E表示eface,I表示iface,T表示自定义的结构体或者基于内建类型创造出的类型。代码都比较简单,不在叙述了。

总结  

想理解interface机制的实现,只需要理解类型元数据以及动态绑定过程。其中要还区分interface value,也就是内部的iface结构体。因此引出了Itable的概念。整体来说不是太复杂,数据结构也比较简单,如果你有时间的话,也可以自己看下GO的源码。

参考

GO源码(go\src\runtime\iface.go)

Go Data Structures: Interfaces

Go Interfaces

时间: 2024-10-25 04:43:22

浅析Go语言的Interface机制的相关文章

浅析Java语言慢的原因

Java在早期(比如JDK1.2以前)是很慢的,后续版本由于有许多优化手段的加入,Java正变得越来越快,所以现在也有很多关于Java和C/C++孰快孰慢的争论.我想就我自己的理解,谈一下影响Java程序运行速度的因素. 1. GC回收 众所周知,Java相对C/C++的一个很大进步就是有了GC机制,它能够很大程序的避免C/C++常见的内存泄漏的发生.但是这也是有代价的,那就是因为JVM管理了所有内存分配释放,当内存不够时就需要做回收,每次回收都有扫描整个堆,然后要搬移一些内存数据,新生代还好,

Java语言的运行机制

计算机语言分为编译型和解释性两种类型. 编译型: ( C/C++.Pascal/Object Pascal(Delphi) ) 编译型语言写的程序执行之前,需要一个专门的编译过程,把程序编译成为机器语言的文件,比如exe文件,以后要运行的话就不用重新翻译了,直接使用编译的结果就行了(exe文件),因为翻译只做了一次,运行时不需要翻译,所以编译型语言的程序执行效率高. 解释性: ( Java.JavaScript.VBScript.Perl.Python.Ruby.MATLAB ) 解释性语言在运

Java记录 -83- Java语言的反射机制

Java语言的反射机制 在Java运行时环境中,对于任意一个类,能否知道这个类有哪些属性和方法?对于任意一个对象,能否调用它的任意一个方法?答案是肯定的.这种动态获取类的信息以及动态调用对象的方法的功能来自于Java语言的反射(Reflection)机制. Java反射机制主要提供了以下功能: 1. 在运行时判断任意一个对象所属的类: 2. 在运行时构造任意一个类的对象: 3. 在运行时判断任意一个类所具有的成员变量和方法: 4. 在运行时调用任意一个对象的方法: Reflection是java

《Java疯狂讲义》(第3版)学习笔记 2 - Java语言的运行机制

内容 1.高级语言的运行机制 2.Java 语言的运行机制 1.高级语言的运行机制 高级语言主要分为编译型语言和解释型语言两类. 编译型语言是指使用专门的编译器.针对特定平台(操作系统)将高级语言源代码一次性“翻译”成该平台硬件执行的机器码(包括机器指令和操作数),并包装成该平台所能识别的可执行文件格式,这个转换过程称为编译(Compile).编译生成的可执行程序可以脱离开发环境,在特定的平台下独立运行.有些还需要链接其他编译好的目标代码 编译型语言是程序在执行之前需要一个专门的编译过程,把程序

java语言实现跨平台机制的原因

JVM(java虚拟机的发展史): (1)Sun Classic classic jvm要么采用纯解释器解释执行,要么采用JIT编译执行,一旦JIT进行编译执行,则解释器不再生效 如果使用JIT编译代码,则JIT会对每个方法,每行代码都进行编译,对于那种只需运行一次,不具有编译价值的代码,也会被JIT编译执行.迫于程序响应时间的压力,此阶段的JIT不敢采用编译耗时的优化技术,所以及时采用JIT输出本地代码,他的执行效率也和C代码有很大差距.被人诟病"java语言很慢" (2)Exact

第一章 第二节 Java语言的运行机制

1.Java语言的运行机制 首先,我们介绍一下什么是计算机语言.对于计算机来说,真正能够直接执行的是所谓的"计算机指令".这种计算机指令,一方面跟着操作系统有关,也就是说,Windows系统和Linux系统下的指令不同.另一方面说,也跟计算机的硬件有关系,不同的CPU具有不同的指令集. 直接操作计算机指令,使用的是计算机语言以及汇编语言.然而,对于程序员来说,直接使用汇编语言来编写程序进行开发是非常慢的,为了能让程序开发的速度提升,设计出了计算机高级语言. 所谓的计算机高级语言,实际上

浅解 go 语言的 interface(许的博客)

我写了一个 go interface 相关的代码转换为 C 代码的样例.也许有助于大家理解 go 的 interface.不过请注意一点,这里没有完整解析 go 语言 interface 的所有细节. Go 代码: package main import "fmt" // ------------------------------------------------------------- type IReadWriter interface {     Read(buf *byt

浅析Java虚拟机结构与机制

本文旨在给所有希望了解JVM(Java Virtual Machine)的同学一个概念性的入门,主要介绍了JVM的组成部分以及它们内部工作的机制和原理.当然本文只是一个简单的入门,不会涉及过多繁杂的参数和配置,感兴趣的同学可以做更深入的研究,在研究JVM的过程中会发现,其实JVM本身就是一个计算机体系结构,很多原理和我们平时的硬件.微机原理.操作系统都有十分相似的地方,所以学习JVM本身也是加深自我对计算机结构认识的一个很好的途径. 另外需要注意的是,虽然平时我们用的大多是Sun(现已被Orac

浅析OC语言

学习一门开发语言,首先要掌握的它的基本语法,这可能几天就能学会,但如果要融会贯通,就得去学习这门语言的框架和一些库,再结合一些项目的应用,这可能需要花几年的时间. OC是C语言的一个超集,是一门面向对象的语言,因为苹果的崛起而火,API主要是cocoa(OSX)和cocoatouch(iOS),GCC 和 Clang 都能编译OC,现在xcode使用的是Clang. OC的实现是建立在运行时机制(runtime system)之上的(runtime system 使用C写的),使它区别与C++编