浅析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语言的Interface机制的更多相关文章
- 《Java疯狂讲义》(第3版)学习笔记 2 - Java语言的运行机制
内容 1.高级语言的运行机制 2.Java 语言的运行机制 1.高级语言的运行机制 高级语言主要分为编译型语言和解释型语言两类. 编译型语言是指使用专门的编译器.针对特定平台(操作系统)将高级语言源代 ...
- Go语言之Interface(一)
Go语言之Interface(一) 什么是interface 在面向对象语言中接口是:接口定义了一个对象的行为,但在Go中接口就是方法签名的集合,当一个类型提供了这个接口中的所有的方法,就可以说这个类 ...
- Atitit.跨语言异常转换机制 java c# php到js的异常转换
Atitit.跨语言异常转换机制 java c# php到js的异常转换 1. bizEx 直接抓取,然后js catchEX1 2. Chkec runtimeEx1 3. Other异常..J ...
- C语言使用信号量机制实例:
C语言使用信号量机制实例: #include <signal.h> #include <unistd.h> #include <stdio.h> #include ...
- 浅析Java虚拟机结构与机制[转]
本文旨在给所有希望了解JVM(Java Virtual Machine)的同学一个概念性的入门,主要介绍了JVM的组成部分以及它们内部工作的机制和原理.当然本文只是一个简单的入门,不会涉及过多繁杂的参 ...
- 浅析OC语言
学习一门开发语言,首先要掌握的它的基本语法,这可能几天就能学会,但如果要融会贯通,就得去学习这门语言的框架和一些库,再结合一些项目的应用,这可能需要花几年的时间. OC是C语言的一个超集,是一门面向对 ...
- OC:浅析Runtime中消息转发机制
一.介绍 OC是一门动态性语言,其实现的本质是利用runtime机制.在runtime中,对象调用方法,其实就是给对象发送一个消息,也即objc_msgSend().在这个消息发送的过程中,系统会进行 ...
- 浅解 go 语言的 interface(许的博客)
我写了一个 go interface 相关的代码转换为 C 代码的样例.也许有助于大家理解 go 的 interface.不过请注意一点,这里没有完整解析 go 语言 interface 的所有细节. ...
- C语言栈调用机制初探
学习linux离不开c语言,也离不开汇编,二者之间的相互调用在源代码中几乎随处可见.所以必须清楚地理解c语言背后的汇编结果才能更好地读懂linux中相关的代码.否则会有很多疑惑,比如在head.s中会 ...
随机推荐
- Java 线程
线程:线程是进程的组成部分,一个进程可以拥有多个线程,而一个线程必须拥有一个父进程.线程可以拥有自己的堆栈,自己的程序计数器和自己的局部变量,但不能拥有系统资源.它与父进程的其他线程共享该进程的所有资 ...
- 继电器是如何成为CPU的(1)
继电器是如何成为CPU的(1) ——<穿越计算机的迷雾>整理和总结 究竟是如何设计的电路,具有计算和控制的智力? 这一点也不高深.本系列文章从初中学的最简单的电路图说起,看看能不能从最初的 ...
- CSS Position 定位属性
本篇文章主要介绍元素的Position属性,此属性可以设置元素在页面的定位方式. 目录 1. 介绍 position:介绍position的值以及辅助属性. 2. position 定位方式:介绍po ...
- nginx的使用
1.nginx的下载 解压后文件目录: 2.nginx的常用命令 nginx -s stop 强制关闭 nginx -s quit 安全关闭 nginx -s reload 改变配置文件的时候,重 ...
- ASP.NET Core中如影随形的”依赖注入”[下]: 历数依赖注入的N种玩法
在对ASP.NET Core管道中关于依赖注入的两个核心对象(ServiceCollection和ServiceProvider)有了足够的认识之后,我们将关注的目光转移到编程层面.在ASP.NET ...
- redis 学习笔记(1)
redis持久化 snapshot数据快照(rdb) 这是一种定时将redis内存中的数据写入磁盘文件的一种方案,这样保留这一时刻redis中的数据镜像,用于意外回滚.redis的snapshot的格 ...
- 深入理解 Java G1 垃圾收集器--转
原文地址:http://blog.jobbole.com/109170/?utm_source=hao.jobbole.com&utm_medium=relatedArticle 本文首先简单 ...
- Linux常用命令操作
系统信息 arch 显示机器的处理器架构(1) uname -m 显示机器的处理器架构(2) uname -r 显示正在使用的内核版本 dmidecode -q 显示硬件系统部件 - (SMBIOS ...
- 设计模式之行为类模式大PK
行为类模式大PK 行为类模式包括责任链模式.命令模式.解释器模式.迭代器模式.中介者模式.备忘录模式.观察者模式.状态模式.策略 ...
- Angular (SPA) WebPack模块化打包、按需加载解决方案完整实现
文艺小说-?2F,言情小说-?3F,武侠小说-?9F long long ago time-1-1:A 使用工具,long long A ago time-1-2:A 使用分类工具,long long ...