在上一篇文章《go interface 基本用法》中,我们了解了 go 中 interface
的一些基本用法,其中提到过 接口本质是一种自定义类型
,本文就来详细说说为什么说 接口本质是一种自定义类型
,以及这种自定义类型是如何构建起 go 的 interface
系统的。
本文使用的源码版本: go 1.19。另外本文中提到的
interface
和接口
是同一个东西。
前言
在了解 go interface
的设计过程中,看了不少资料,但是大多数资料都有生成汇编的操作,但是在我的电脑上指向生成汇编的操作的时候,
生成的汇编代码却不太一样,所以有很多的东西无法验证正确性,这部分内容不会出现在本文中。本文只写那些经过本机验证正确的内容,但也不用担心,因为涵盖了 go interface
设计与实现的核心部分内容,但由于水平有限,所以只能尽可能地传达我所知道的关于 interface
的一切东西。对于有疑问的部分,有兴趣的读者可以自行探索。
如果想详细地了解,建议还是去看看 iface.go
,里面有接口实现的一些关键的细节。但是还是有一些东西被隐藏了起来,
导致我们无法知道我们 go 代码会是 iface.go
里面的哪一段代码实现的。
接口是什么?
接口(
interface
)本质上是一种结构体。
我们先来看看下面的代码:
// main.go
package main
type Flyable interface {
Fly()
}
// go tool compile -N -S -l main.go
func main() {
var f1 interface{
}
println(f1) // CALL runtime.printeface(SB)
var f2 Flyable
println(f2) // CALL runtime.printiface(SB)
}
我们可以通过 go tool compile -N -S -l main.go
命令来生成 main.go
的伪汇编代码,生成的代码会很长,下面省略所有跟本文主题无关的代码:
// main.go:10 => println(f1)
0x0029 00041 (main.go:10) CALL runtime.printeface(SB)
// main.go:13 => println(f2)
0x004f 00079 (main.go:13) CALL runtime.printiface(SB)
我们从这段汇编代码中可以看到,我们 println(f1)
实际上是对 runtime.printeface
的调用,我们看看这个 printeface
方法:
func printeface(e eface) {
print("(", e._type, ",", e.data, ")")
}
我们看到了,这个 printeface
接收的参数实际上是 eface
类型,而不是 interface{}
类型,我们再来看看 println(f2)
实际调用的 runtime.printiface
方法:
func printiface(i iface) {
print("(", i.tab, ",", i.data, ")")
}
也就是说 interface{}
类型在底层实际上是 eface
类型,而 Flyable
类型在底层实际上是 iface
类型。
这就是本文要讲述的内容,go 中的接口变量其实是用 iface
和 eface
这两个结构体来表示的:
iface
表示某一个具体的接口(含有方法的接口)。eface
表示一个空接口(interface{}
)
iface 和 eface 结构体
iface
和 eface
的结构体定义(runtime/iface.go
):
// 非空接口(如:io.Reader)
type iface struct {
tab *itab // 方法表
data unsafe.Pointer // 指向变量本身的指针
}
// 空接口(interface{})
type eface struct {
_type *_type // 接口变量的类型
data unsafe.Pointer // 指向变量本身的指针
}
go 底层的类型信息是使用
_type
结构体来存储的。
比如,我们有下面的代码:
package main
type Bird struct {
name string
}
func (b Bird) Fly() {
}
type Flyable interface {
Fly()
}
func main() {
bird := Bird{
name: "b1"}
var efc interface{
} = bird // efc 是 eface
var ifc Flyable = bird // ifc 是 iface
println(efc) // runtime.printeface
println(ifc) // runtime.printiface
}
在上面代码中,efc
是 eface
类型的变量,对应到 eface
结构体的话,_type
就是 Bird
这个类型本身,而 data
就是 &bird
这个指针:
类似的,ifc
是 iface
类型的变量,对应到 iface
结构体的话,data
也是 &bird
这个指针:
_type 是什么?
在 go 中,_type
是保存了变量类型的元数据的结构体,定义如下:
// _type 是 go 里面所有类型的一个抽象,里面包含 GC、反射、大小等需要的细节,
// 它也决定了 data 如何解释和操作。
// 里面包含了非常多信息:类型的大小、哈希、对齐及 kind 等信息
type _type struct {
size uintptr // 数据类型共占用空间的大小
ptrdata uintptr // 含有所有指针类型前缀大小
hash uint32 // 类型 hash 值;避免在哈希表中计算
tflag tflag // 额外类型信息标志
align uint8 // 该类型变量对齐方式
fieldAlign uint8 // 该类型结构体字段对齐方式
kind uint8 // 类型编号
// 用于比较此类型对象的函数
equal func(unsafe.Pointer, unsafe.Pointer) bool
// gc 相关数据
gcdata *byte
str nameOff // 类型名字的偏移
ptrToThis typeOff
}
这个 _type
结构体定义大家随便看看就好了,实际上,go 底层的类型表示也不是上面这个结构体这么简单。
但是,我们需要知道的一点是(与本文有关的信息),通过 _type
我们可以得到结构体里面所包含的方法这些信息。
具体我们可以看 itab
的 init
方法(runtime/iface.go
),我们会看到如下几行:
typ := m._type
x := typ.uncommon() // 结构体类型
nt := int(x.mcount) // 实际类型的方法数量
// 实际类型的方法数组,数组元素为 method
xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
在底层,go 是通过 _type
里面 uncommon
返回的地址,加上一个偏移量(x.moff
)来得到实际结构体类型的方法列表的。
我们可以参考一下下图想象一下:
itab 是什么?
我们从 iface
中可以看到,它包含了一个 *itab
类型的字段,我们看看这个 itab
的定义:
// 编译器已知的 itab 布局
type itab struct {
inter *interfacetype // 接口类型
_type *_type
hash uint32
_ [4]byte
fun [1]uintptr // 变长数组. fun[0]==0 意味着 _type 没有实现 inter 这个接口
}
// 接口类型
// 对应源代码:type xx interface {}
type interfacetype struct {
typ _type // 类型信息
pkgpath name // 包路径
mhdr []imethod // 接口的方法列表
}
根据
interfacetype
我们可以得到关于接口所有方法的信息。同样的,通过_type
也可以获取结构体类型的所有方法信息。
从定义上,我们可以看到 itab
跟 *interfacetype
和 *_type
有关,但实际上有什么关系从定义上其实不太能看得出来,
但是我们可以看它是怎么被使用的,现在,假设我们有如下代码:
// i 在底层是一个 interfacetype 类型
type i interface {
A()
C()
}
// t 底层会用 _type 来表示
// t 里面有 A、B、C、D 方法
// 因为实现了 i 中的所有方法,所以 t 实现了接口 i
type t struct {
}
func (t) A() {
}
func (t) B() {
}
func