浅析Go语言的Interface机制
前几日一朋友在学GO,问了我一些interface机制的问题。试着解释发现自己也不是太清楚,所以今天下午特意查了资料和阅读GO的源码(基于go1.4),整理出了此文。如果有错误的地方还望指正。
GO语言的interface是我比较喜欢的特性之一。interface与struct之间可以相互转换,struct不需要像JAVA在源码中显示说明实现了某个接口,可以通过约定的形式,隐式的转换到interface,还可以在运行时查询接口类型,这样有种用动态语言写代码的感觉,但是又可以在编译时进行检查,捕捉一些明显的类型不匹配的错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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。它的数据结构如下:
1 2 3 4 | type iface struct { tab *itab data unsafe.Pointer } |
其中tab字段类似于C++的vptr,tab中包含了对应的方法数组,除此之外还保存了实现该接口的类型元数据。data是对应的实现该接口的类型的实例指针。
itab数据结构如下:
1 2 3 4 5 6 7 8 | 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{}对象的类型原型如下:
1 2 3 4 | 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)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?