概述
接口是Go
语言编程中数据类型的关键。在Go
语言的实际编程中,几乎所有的数据结构都围绕接口展开,接口是Go
语言中所有数据结构的核心。Go
语言中的接口实际上是一组方法的集合,接口和gomock
配合使用可以使得我们写出易于测试的代码.但是除了在反射等使用场景中我们很难直接感知到接口的存在(虽然大多数人使用反射的时候也没有感知到接口在其中发挥的作用),但是想要深入理解Go语言,我们必须对接口有足够的了解.接下来我们将从接口的数据结构、结构体如何转变成interface
和Go
语言中动态派发的实现这些方面来一起学习Go
语言中的接口。
隐身接口
很多面向对象语言都有接口这一概念,例如 Java
。Java
的接口不仅可以定义方法签名,还可以定义变量,这些定义的变量可以直接在实现接口的类中使用,这里简单介绍一下 Java
中的接口:
public interface MyInterface {
public String hello = "Hello";
public void sayHello();
}
上述代码定义了一个必须实现的方法 sayHello
和一个会注入到实现类的变量 hello
。在下面的代码中,MyInterfaceImpl
实现了 MyInterface
接口:
public class MyInterfaceImpl implements MyInterface {
public void sayHello() {
System.out.println(MyInterface.hello);
}
}
Java
中的类必须通过上述方式显式地声明实现的接口,但是在 Go
语言中实现接口就不需要使用类似的方式。
在 Go
语言中如何定义接口。定义接口需要使用 interface
关键字,在接口中我们只能定义方法签名,不能包含成员变量,一个常见的 Go
语言接口是这样的:
type Animal interface {
Say() string
Name() string
}
如果一个类型需要实现 Animal
接口,那么它只需要实现 Say() string
和 Name() string
方法,下面的 Duck
结构体就是接口的一个实现:
type Duck struct {
Name string
Sound string
}
func (a *Duck) MySay() string {
return fmt.Sprintf("My Sound is: %s", a.Sound)
}
func (a *Duck) MyName() string {
return fmt.Sprintf("My Name is: %s", a.Name)
}
上述代码根本就没有 Animal
接口的影子,这是为什么呢?Go 语言中接口的实现都是隐式的,我们只需要实现 MySay() string
和 Name() string
方法就实现了 Animal
接口。
指针和结构体接收者
我们经常能看到两种实现接口的接收方式:指针和结构体,看下面缩略代码:
type Animal interface {
MySay() string
MyName() string
}
type Duck struct {...}
//指针方式
func (a *Duck) MySay() string {...}
func (a *Duck) MyName() string {...}
//结构体方式
func (a Duck) MySay() string {...}
func (a Duck) MyName() string {...}
因为结构体类型和指针类型是不同的,但是上面两种实现不可以同时存在,Go
语言的编译器会在结构体类型和指针类型都实现一个方法时报错 method redeclared
。
实现接口的类型和初始化返回的类型两个维度共组成了四种情况,然而这四种情况不是都能通过编译器的检查:
结构体接收者- func (a Duck) MySay() | 指针接收者 - func (a *Duck) MySay() | |
---|---|---|
结构体指针方式初始化 var Duck Animal = &Duck{} | 通过检查 | 通过检查 |
结构体方式初始化 var Duck Animal = Duck{} | 通过检查 | 不通过 |
如上表所示,无论上述代码中初始化的变量 是 Duck{}
还是 &Duck{}
,使用 MySay()
调用方法时都会发生值拷贝:
-
对于
&Duck{}
来说,这意味着拷贝一个新的&Duck{}
指针,这个指针与原来的指针指向一个相同并且唯一的结构体,所以编译器可以隐式的对变量解引用(dereference
)获取指针指向的结构体; -
对于
Duck{}
来说,这意味着MySay
方法会接受一个全新的Duck{}
,因为方法的参数是*Cat
,编译器不会无中生有创建一个新的指针;即使编译器可以创建新指针,这个指针指向的也不是最初调用该方法的结构体;
总结起来:当我们使用指针实现接口时,只有指针类型的变量才会实现该接口;当我们使用结构体实现接口时,指针类型和结构体类型都会实现该接口。
接口嵌套
在Go
语言中,不仅结构体与结构体之间可以嵌套,接口与接口间也可以通过嵌套创造出新的接口。
一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。只要接口的所有方法被实现,则这个接口中的所有嵌套接口的方法均可以被调用。
Go
语言的 io
包中定义了写入器(Writer
)、关闭器(Closer
)和写入关闭器(WriteCloser
)3 个接口,代码如下:
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type WriteCloser interface {
Writer
Closer
}
在代码中使用 io.Writer
、io.Closer
和 io.WriteCloser
这 3 个接口时,只需要按照接口实现的规则实现 io.Writer
接口和 io.Closer
接口即可。而 io.WriteCloser
接口在使用时,编译器会根据接口的实现者确认它们是否同时实现了 io.Writer
和 io.Closer
接口,详细实现代码如下:
package main
import (
"io"
)
// 声明一个设备结构
type device struct {
}
// 实现io.Writer的Write()方法
func (d *device) Write(p []byte) (n int, err error) {
return 0, nil
}
// 实现io.Closer的Close()方法
func (d *device) Close() error {
return nil
}
func main() {
// 声明写入关闭器, 并赋予device的实例
var wc io.WriteCloser = new(device)
// 写入数据
wc.Write(nil)
// 关闭设备
wc.Close()
// 声明写入器, 并赋予device的新实例
var writeOnly io.Writer = new(device)
// 写入数据
writeOnly.Write(nil)
}
nil 与 non-nil
Go 语言的接口类型不是任意类型。
我们可以通过一个例子理解这句话,下面的代码在 main
函数中初始化了一个 *NilStruct
类型的变量,由于指针的零值是 nil
,所以变量 t
在初始化之后也是 nil
:
package main
import "fmt"
type NilStruct struct {
}
func NilOrNot(v interface{}) bool {
if v == nil {
return true
}
return false
}
func main() {
var t *NilStruct
fmt.Println(t == nil)
fmt.Println(NilOrNot(t))
}
$ go run main.go
true
false
我们简单总结一下上述代码执行的结果:
- 将上述变量与
nil
比较会返回true
; - 将上述变量传入
NilOrNot
方法并与nil
比较会返回false
;
出现上述现象的原因是 —— 调用 NilOrNot
函数时发生了隐式的类型转换,除了向方法传入参数之外,变量的赋值也会触发隐式类型转换。在类型转换时,*NilStruct
类型会转换成 interface{}
类型,转换后的变量不仅包含转换前的变量,还包含变量的类型信息 NilStruct
,所以转换后的变量与 nil
不相等。
数据结构
类型
在 Go
语言中,类型(Type
)是用来描述数据的属性和操作的概念。它定义了数据的内部表示以及对数据进行操作的方法。类型在编程语言中起到了限制和约束数据的作用,它决定了数据的取值范围、可用的操作,以及数据在内存中的布局方式。
在 Go
语言中,每个值都有一个明确的类型。例如,整数类型(int
)、浮点数类型(float64
)、布尔类型(bool
)、字符串类型(string
)等都是 Go 语言中的内置类型。此外,我们还可以自定义结构体类型(struct
)和接口类型(interface
),以及通过类型别名(type
)来创建自定义类型。
在类型中都有一些公有属性,例如类型的大小、对齐方式、哈希值、标志位、种类、相等性函数、垃圾回收数据、名称和指针等,是所有类型最原始的元信息。这些元信息,记录在位于src/runtime/type.go
的_type
结构体中,作为每个类型元数据的Header
:
//go 1.20.3 path: /src/runtime/type.go
type _type struct {
size uintptr //表示类型的大小,即占用内存的字节数
ptrdata uintptr //类型中指针数据的大小,以字节为单位。这个字段用于垃圾回收器识别类型中哪些部分是指针,哪些部分是非指针
hash uint32 //类型的哈希值,用于在运行时比较两个类型是否相同
tflag tflag //类型的标志位,用于存储一些额外的信息,如是否有名称、是否有不可比较的字段等
align uint8 //类型的对齐方式,以字节为单位。这个字段决定了类型在内存中的布局和对齐方式
fieldAlign uint8 //类型的字段对齐方式,以字节为单位。这个字段决定了类型中的字段在内存中的布局和对齐方式
kind uint8 //类型的种类,用于区分不同的基本类型,如 int、string、struct 等
equal func(unsafe.Pointer, unsafe.Pointer) bool //类型的相等性函数,用于在运行时比较两个值是否相等。如果为 nil,则表示该类型没有定义相等性函数,或者该类型是不可比较的
gcdata *byte //类型的垃圾回收数据,用于存储一些与垃圾回收相关的信息,如指针位图等
str nameOff //类型的名称偏移量,用于在运行时获取类型的名称。这个字段是一个相对于 _type 结构体起始地址的偏移量,可以通过它找到一个 nameOff 结构体,进而找到一个 name 结构体,其中存储了类型的名称
ptrToThis typeOff //类型的指针偏移量,用于在运行时获取指向该类型的指针类型。这个字段也是一个相对于 _type 结构体起始地址的偏移量,可以通过它找到一个 typeOff 结构体,进而找到一个 _type 结构体,其中存储了指向该类型的指针类型
}
_type
结构体是 Go
语言中实现反射机制和接口机制的基础。反射机制可以让我们在运行时动态地获取和操作任何值和类型的信息。接口机制可以让我们实现多态性和抽象性,让不同的类型可以实现相同的行为。
有个别字段需要详细说明下,其他字段直接备注:
-
kind 基础类型,在
Go
语言中,基础类型是一个枚举常量,有26
个基础类型,枚举值通过kindMask
取出特殊标记位const ( kindBool = 1 + iota kindInt kindInt8 kindInt16 kindInt32 kindInt64 kindUint kindUint8 kindUint16 kindUint32 kindUint64 kindUintptr kindFloat32 kindFloat64 kindComplex64 kindComplex128 kindArray kindChan kindFunc kindInterface kindMap kindPtr kindSlice kindString kindStruct kindUnsafePointer kindDirectIface = 1 << 5 kindGCProg = 1 << 6 kindMask = (1 << 5) - 1 )
-
str
和ptrToThis
,对应的类型是nameoff
和typeOff
。分表name
和type
针对最终输出文件所在段内的偏移量。在编译的链接步骤中,链接器将各个.o
文件中的段合并到输出文件,会进行段合并,有的放入.text
段,有的放入.data
段,有的放入.bss
段。nameoff
和typeoff
就是记录了对应段的偏移量。
对于int
、string
、bool
等单一的基础结构,元信息存储于_type
结构体内已经够用了,但对于 array
、 chan
、 slice
、 func
等复合型的结构体,它们除了基础的元信息,还需要存储一些额外的元数据,比如键和值类型、参数和返回数量、结构体字段等,为了存储这些信息,golang
设定了很多内置类型,来处理不同类型需要存储不同信息的需求。
对于内置类型,大部分也都在runtime.type
文件里面,些内置类型都是在 _type
基础上进一步封装而来,列举个别例子说明:
//go 1.20.3 path: /src/runtime/type.go
//用于表示数组类型
type arraytype struct {
typ _type // 类型描述符
elem *_type // 数组元素类型的指针
slice *_type // 切片类型的指针
len uintptr // 数组长度
}
//用于表示通道类型
type chantype struct {
typ _type // 类型描述符
elem *_type // 通道元素类型的指针
dir uintptr // 通道的方向
}
//表示切片类型
type slicetype struct {
typ _type // 类型描述符
elem *_type // 切片元素类型的指针
}
//用于表示函数类型
type functype struct {
typ _type // 类型描述符
inCount uint16 // 输入参数数量
outCount uint16 // 输出参数数量
}
//用于表示指针类型
type ptrtype struct {
typ _type // 类型描述符
elem *_type // 指针指向的类型
}
//用于表示结构体类型
type structtype struct {
typ _type
pkgPath name // 结构体所属包的路径
fields []structfield //结构体的字段列表
}
大家可能疑问,上面都是go
语言里面的内置类型,那我们在代码中自己定义的类型是什么样的呢?
还有如果是自定义类型,后面还会有一个uncommontype
结构体,uncommontype
是指向一个函数指针的数组,收集了这类型的实现的所有方法:
//自定义类型元数据
type uncommontype struct {
pkgpath nameOff
mcount uint16 // number of methods
xcount uint16 // number of exported methods
moff uint32 // offset from this uncommontype to [mcount]method
_ uint32 // unused
}
- pkgpath 记录类型所在的包路径
- mcount 记录该类型关联到多少个方法
- xcount 记录该类型的导出型多少个方法
- moff 记录的是这些方法元数据组成的数组,相对于这个
uncommontype
结构体偏移了多少字节
例如,我们基于[]string
定义一个新类型myslice
,它就是一个自定义类型,可以给它定义两个方法Len
和Cap
。
myslice
的类型元数据中,首先是slicetype
类型描述信息,然后在后面加上uncommontype
结构体。注意通过uncommontype
这里记录的信息,我们就可以找到myslice
的方法元数据列表了。如下图所示:
![image-20220829152709907](https://xjxpicgo.oss-cn-hangzhou.aliyuncs.com/image-20220829152709907.png)
接口实现
对于golang
来说有两种接口的结构,一种是有方法定义的接口,另外一种是空接口,分别对应两种实现。
- 使用
runtime.iface
结构体表示包含方法的接口 - 使用
runtime.eface
结构体表示不包含任何方法的interface{}
类型;
eface — 空Interface
空接口通过eface
结构体定义实现:
//go1.20.1 path: src/runtime/runtime2.go
type eface struct {
_type *_type //类型元数据
data unsafe.Pointer //数据信息,指向数据指针
}
eface
包含了2个元素:
-
_type
,指向对象的类型元数据,在编译的时候生成在可执行文件后随着可执行文件加载进入.text
的.rodata
区域内; -
data
,指向数据指针。
举个例子:
package main
import "fmt"
//Student结构体
type Student struct {
name string
}
//Student方法setName
func (s *Student) setName(name string) {
s.name = name
}
//Student方法getName
func (s *Student) getName() string {
return s.name
}
func main() {
//声明一个空接口变量a
var a interface{}
s := &Student{"Jack"}
a = s
fmt.Println(a)
}
我们把Student
类型的变量s
赋给a
。那么变量a
的结构就如下图所示:
我们通过 gdb
调试信息查看验证是否正确:
(gdb) info locals
a = {_type = 0x10c0980 <type:*+46208>, data = 0xc000014270}
s = 0xc000014270
空interface
相对比较简单,介绍到此。
iface — 非空Interface
非空接口使用的是runtime.iface
这个结构体 :
//go1.20.1 path: src/runtime/runtime2.go
type iface struct {
tab *itab
data unsafe.Pointer //指向原始数据指针
}
同样包含两个字段:
-
tab
,存放的是类型、方法等信息;tab
所对应的结构体是runtime.itab
,该结构体是接口类型的核心组成部分;每一个runtime.itab
都占 32 字节,我们可以将其看成接口类型和具体类型的组合,它们分别用inter
和_type
两个字段表示://go1.20.1 path: src/runtime/runtime2.go type itab struct { inter *interfacetype // 接口自身定义的类型信息,用于定位到具体interface类型 _type *_type // 接口的具体类型,指向实际对象类型 hash uint32 //_type.hash的拷贝,用于快速查询和判断目标类型和接口中类型是一致 _ [4]byte // 填充字段,保证对齐用 fun [1]uintptr //动态数组,接口方法实现列表(方法集),即函数地址列表,按字典序排序,如果数组中的内容为空表示 _type 没有实现 inter 接口 }
-
inter
interfacetype
的类型元数据;它里面记录了这个接口类型的描述信息,接口要求的方法列表就记录在interfacetype.mhdr
,结构体代码如下:type interfacetype struct { typ _type //接口类型 pkgpath name //包路径 mhdr []imethod //接口中的方法表 }
其中
interfacetype
结构体的mhdr
字段涉及到imethod
类型,imethod
结构体用于描述接口类型的方法;method
结构体用于描述类型的方法。我们来看下其结构:
//接口类型的方法 type imethod struct { name nameOff // 方法名称在名称表中的偏移量 ityp typeOff // 方法类型在类型表中的偏移量 } //非接口类型的方法,它是一个压缩格式的结构,每个字段的值都是一个相对偏移量 type method struct { name nameOff // 方法名称在名称表中的偏移量 mtyp typeOff // 方法类型在类型表中的偏移量 ifn textOff // 接口方法的实现函数在代码段中的偏移量 tfn textOff // 普通方法的实现函数在代码段中的偏移量 }
-
hash 从
itab._type
中拷贝来的,是类型的哈希值,用于快速判断类型是否相等时使用; -
fun 它是一个用于动态派发的虚函数表,存储了一组函数指针; 记录的是动态类型实现的那些接口要求的方法的地址,是从方法元数据中拷贝来的,为的是快速定位到方法。如果
itab._type
对应的类型没有实现这个接口,则itab.fun[0]=0
; 当fun[0]为0时,说明_type并没有实现该接口,当有实现接口时,fun存放了第一个接口方法的地址,其他方法一次往下存放,这里就简单用空间换时间,其实方法都在_type字段中能找到,实际在这记录下,每次调用的时候就不用动态查找了。
-
-
data
指针指向绑定对象的原始数据;
下面我们看一个例子,代码如下:
package main
import (
"fmt"
)
type Square interface {
Area() float64
Perimeter() float64
}
type Sdata struct {
x, y float64
}
func (s *Sdata) Area() float64 {
return s.x * s.y
}
func (s *Sdata) Perimeter() float64 {
return (s.x + s.y) * 2
}
func NewSdata(x, y float64) *Sdata {
return &Sdata{
x: x,
y: y,
}
}
func main() {
var s Square
Object := NewSdata(1, 2)
s = Object
fmt.Println(s)
}
通过 gdb
调试信息,我们看下变量s
赋值前的结果:
----------- 赋值前 ---------------
(gdb) p s
$2 = {tab = 0x0, data = 0x0}
----------- 赋值后 ---------------
(gdb) p s
$1 = {tab = 0x10dd9b8 <go:itab.*main.Sdata,main.Square>, data = 0xc0000b4010}
(gdb) ptype s
type = struct runtime.iface {
runtime.itab *tab;
void *data;
}
(gdb) p Object
$2 = (main.Sdata *) 0xc0000b4010
从调试结果来看, s
为非空接口,赋值前对应结构和数据:{tab = 0x0, data = 0x0}
, 赋值后对应的数据和结构:{tab = 0x10dd9b8 <go:itab.*main.Sdata,main.Square>, data = 0xc0000b4010}
,用图表示关系如下:
![image-20220825175647238](https://xjxpicgo.oss-cn-hangzhou.aliyuncs.com/image-20220825175647238.png)
至此,非空接口的关系以及逻辑关联已经讲清楚了,后续将对itab
进行详细讲解。
itab结构
itab缓存(itabTable)
对于itab
来说,既然一个非空的接口类型(itab.inter
) 和一个动态类型(itab._type
) 就可以确定一个itab
的内容,那么这个itab
结构体自然是可以被接口类型和动态类型均相同的接口变量进行复用的。这就是itabTable
, itabTable
结构如下:
const itabInitSize = 512
type itabTableType struct {
size uintptr // length of entries array. Always a power of 2.
count uintptr // current number of filled entries.
entries [itabInitSize]*itab // really [size] large
}
- size 数组的大小,即
entries
数组的大小,长度总数为 2n - count 记录当前实际的itab数量,即
entries
数组使用了多少量 - entries 一个
*itab
数组,初始大小是itabInitSize
(512)个, 即利用空间换时间的思路,存放所有的itab
itab初始化
go的启动过程中,schedinit
里面会调用的itabsinit
进行初始化,代码如下:
func schedinit() {
......
itabsinit() // uses activeModules
......
}
func itabsinit() {
lockInit(&itabLock, lockRankItab)
lock(&itabLock)
for _, md := range activeModules() {
for _, i := range md.itablinks {
itabAdd(i)
}
}
unlock(&itabLock)
}
简单理解 itabsinit()
这个方法的作用是将各模块间用于缓存运行时类型转换的接口表初始化到itabTable
中。
itabAdd
itabAdd()
方法是将单条 itab
数据 存入到 itabTable
中,代码如下:
const itabInitSize = 512
var (
......
itabTable = &itabTableInit
itabTableInit = itabTableType{size: itabInitSize}
)
//itabAdd将给定的itab添加到itab哈希表中
func itabAdd(m *itab) {
// 检查是否存在 malloc 死锁
if getg().m.mallocing != 0 {
throw("malloc deadlock")
}
t := itabTable
if t.count >= 3*(t.size/4) { //当itab哈希表中使用率超过75%时,需要扩容处理
//以原来2倍的空间去申请新的itab哈希表空间
t2 := (*itabTableType)(mallocgc((2+2*t.size)*sys.PtrSize, nil, true))
t2.size = t.size * 2
//将旧的itab哈希表数据复制到新的itab哈希表中
iterate_itabs(t2.add)
if t2.count != t.count {
throw("mismatched count during itab table copy")
}
//发布新的哈希表。使用原子写入
atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2))
//采用新表作为作为当前的itab表
t = itabTable
}
//将m添加进itab表
t.add(m)
}
// add将给定的itab添加到itab表t中
func (t *itabTableType) add(m *itab) {
/**
通过与运算法来确定新itab在itab表中的index
与运算法公式:index = hash&(m-1) 其中m-1为数组长度减1,具体参考hashmap章节
*/
mask := t.size - 1 //掩码,用于计算哈希值对应的索引
h := itabHashFunc(m.inter, m._type) & mask // 获取当前索引位置的条目
for i := uintptr(1); ; i++ {
//获取h位置地址
p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize))
//取值
m2 := *p
//如果m信息已经存在,直接返回
if m2 == m {
return
}
//如果找到的位置为空位置,则使用原子插入
if m2 == nil {
atomic.StorepNoWB(unsafe.Pointer(p), unsafe.Pointer(m))
//使用值+1
t.count++
return
}
/**
如果查找的位置已经存在非m值的其余itab信息,则使用开放寻址法,进行重新寻找位置
开放寻址法公式为: h(i) = h0 + i*(i+1)/2 mod 2^k
*/
h += i
h &= mask
}
}
对 itabAdd()
代码总结下:
-
当
itabTable
使用量达到75%时,将会以原大小的双倍大小进行扩容 -
新的
itab
数据插入itabTable
时候,存放位置采取与运算法
:h = hash & (m-1)
; m为数组长度; 当取位存在其他数据时,则使用开放寻址法寻找下一个存放位置;而hash算法使用了itabHashFunc
方法,即 取itab
中的接口类型与实际类型,分别哈希后取异或:func itabHashFunc(inter *interfacetype, typ *_type) uintptr { return uintptr(inter.typ.hash ^ typ.hash) }
getitab & find
getitab()
函数是根据inter
以及 typ
数据 从 itabTable
中获取 itab
的方法,代码如下:
//根据非空的接口类型和动态类型获取itab内容
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
//如果inter.mhdr为空,则表示该接口没有方法和元数据,为空接口,不存在itab
if len(inter.mhdr) == 0 {
throw("internal error - misuse of itab")
}
/**
简单情况
tflagUncommon 标识是否有uncommon内容,即记录pkgpath和方法的内容
目前看来只有匿名结构体或者reflect动态创建的struct没有methods时,该值为0
大致猜测意思就是当前查询的动态类型没有methods时,则直接返回nil
*/
if typ.tflag&tflagUncommon == 0 {
if canfail {
return nil
}
name := inter.typ.nameOff(inter.mhdr[0].name)
panic(&TypeAssertionError{nil, typ, &inter.typ, name.name()})
}
var m *itab
/**
首先,查看现有表以查看是否可以找到所需的itab。
这是迄今为止最常见的情况,因此请不要使用锁。
使用atomic确保我们看到该线程完成的所有先前写入更新itabTable字段(在itabAdd中使用atomic.Storep)。
*/
t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
if m = t.find(inter, typ); m != nil {
goto finish
}
// 如果任然没有找到数据,则加锁重试
lock(&itabLock)
if m = itabTable.find(inter, typ); m != nil {
unlock(&itabLock)
goto finish
}
// 如果条目不存在,则进行输入并添加
m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
m.inter = inter
m._type = typ
m.hash = 0
m.init()
itabAdd(m)
unlock(&itabLock)
finish:
//如果非m.fun[0] == 0,则表明有方法被实现,返回m
if m.fun[0] != 0 {
return m
}
if canfail {
return nil
}
//如果数据类型并没有实现接口,那么根据调用方式,该报错报错,该panic panic。
panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}
//根据信息查找itab信息
func (t *itabTableType) find(inter *interfacetype, typ *_type) *itab {
/**
通过与运算法来确定在itabTable中的index
与运算法公式:index = hash&(m-1) 其中m-1为数组长度减1,具体参考hashmap章节
hash函数为:itabHashFunc
*/
mask := t.size - 1
h := itabHashFunc(inter, typ) & mask
for i := uintptr(1); ; i++ {
p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize))
// 在这里使用atomic read,所以如果我们看到m!= nil,我们也会看到m字段的初始化
// m := *p
m := (*itab)(atomic.Loadp(unsafe.Pointer(p)))
if m == nil {
return nil
}
if m.inter == inter && m._type == typ {
return m
}
/**
使用二次探测
探测公式为h(i)= h0 + i *(i + 1)/ 2 mod 2 ^ k
*/
h += i
h &= mask
}
}
总结下流程:
-
先用
t
保存全局itabTable
的地址,然后使用t.find
去查找,这样是为了防止查找过程中,itabTable
被替换导致查找错误 -
如果没找到,那么就会上锁,然后使用
itabTable.find
去查找,这样是因为在第一步查找的同时,另外一个协程写入,可能导致实际存在却查找不到,这时上锁避免itabTable
被替换,然后直接在itaTable
中查找。 -
再没找到,说明确实没有,那么就根据接口类型、数据类型,去生成一个新的
itab
,然后插入到itabTable
中,这里可能会导致hash
表扩容,如果数据类型并没有实现接口,那么根据调用方式,该报错报错,该panic
panic
。这里我们可以看到申请新的
itab
空间时,内存空间的大小是:unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize
参照前面接受的结构,
len(inter.mhdr)
就是接口定义的方法数量,因为字段fun
是一个大小为1
的数组,所以len(inter.mhdr)-1
,在fun
字段下面其实隐藏了其他方法接口地址。
init
itab.init()
方法是初始化一个itab
的函数,在getitab()
函数中,我们能看到它的身影,如下:
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
......
m.inter = inter
m._type = typ
m.hash = 0
m.init()
itabAdd(m)
......
}
其实 itab
需要初始化之后才能插入itabTable
, 这也就是为什么它出现在 itabAdd
函数之前的原因,遍历接口类型与具体类型比较具体类型是否实现了所有接口类型, 理论上时间复杂度为O(n2),下面我们来看看 init
到底干了些什么,看代码:
//go 1.20.3 path: /src/runtime/iface.go
//初始化itab,填充itab.fun数组
func (m *itab) init() string {
//赋值接口类型
inter := m.inter
//赋值动态类型
typ := m._type
//返回组合的uncommontype类型
x := typ.uncommon()
ni := len(inter.mhdr) //接口类型的方法数量
nt := int(x.mcount) //具体类型的方法数量
//指向具体类型的方法数组
xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
j := 0
//指向具体itab.fun
methods := (*[1 << 16]unsafe.Pointer)(unsafe.Pointer(&m.fun[0]))[:ni:ni]
var fun0 unsafe.Pointer
imethods:
// 这里看似双重遍历,由于方法数组都是有序的,所以其实时间复杂度为ni+nt, 而不是ni*nt
for k := 0; k < ni; k++ { //遍历接口方法
i := &inter.mhdr[k] //获取接口方法i
itype := inter.typ.typeOff(i.ityp) //i方法type
name := inter.typ.nameOff(i.name) //i方法name字段
iname := name.name() // name转化为string
ipkg := name.pkgPath() // 路径转为string
if ipkg == "" {
ipkg = inter.pkgpath.name()
}
for ; j < nt; j++ { // 遍历具体类型的方法
t := &xmhdr[j]
tname := typ.nameOff(t.name)
if typ.typeOff(t.mtyp) == itype && tname.name() == iname { //判等
pkgPath := tname.pkgPath()
if pkgPath == "" {
pkgPath = typ.nameOff(x.pkgpath).name()
}
if tname.isExported() || pkgPath == ipkg {
if m != nil {
ifn := typ.textOff(t.ifn)
if k == 0 {
fun0 = ifn // we'll set m.fun[0] at the end
} else {
methods[k] = ifn
}
}
continue imethods // 存在i方法就continue到外层for
}
}
}
m.fun[0] = 0 // 没有找到i方法, 将m.fun[0] 置为0
return iname
}
m.fun[0] = uintptr(fun0)
return ""
}
这个方法会检查interface
和type
的方法是否匹配,即type
有没有实现interface
。
interface、type的方法都按字典序排,所以O(n+m)的时间复杂度可以匹配完。
在检测的过程中,匹配上了,依次往fun
字段写入type
中对应方法的地址。如果有一个方法没有匹配上,那么就设置fun[0]为0,在外层调用会检查fun[0]==0,即type
并没有实现interface
。
类型转换
指针类型
先直接上代码:
package main
type People interface {
GetName() string
}
//go:noinline
func (s *Student) GetName() string {
return s.name
}
type Student struct {
name string
age int
}
func main() {
var p People
var s *Student = &Student{
name: "XJX",
age: 15,
}
p = s
p.GetName()
}
利用go tool compile
命令将上述代码变成汇编代码:
go tool compile -N -l -S main.go
我们开始分析汇编代码:
-
初始化Student对象
0x0030 00048 (main.go:32) LEAQ type."".Student(SB), AX ;AX = &type."".Student 0x0037 00055 (main.go:32) MOVQ AX, (SP) ;SP = &type."".Student 0x003b 00059 (main.go:32) PCDATA $1, $0 0x003b 00059 (main.go:32) NOP 0x0040 00064 (main.go:32) CALL runtime.newobject(SB) ;SP + 8 = &Student{} 0x0045 00069 (main.go:32) MOVQ 8(SP), DI ;DI = &Student{} 0x004a 00074 (main.go:32) MOVQ DI, ""..autotmp_3+32(SP) ;autotmp_3(32SP) = &Student{} 0x004f 00079 (main.go:33) MOVQ $3, 8(DI) ;StringHeader(DI.Name).Len = 3 0x0057 00087 (main.go:33) PCDATA $0, $-2 0x0057 00087 (main.go:33) CMPL runtime.writeBarrier(SB), $0 0x005e 00094 (main.go:33) NOP 0x0060 00096 (main.go:33) JEQ 100 0x0062 00098 (main.go:33) JMP 191 0x0064 00100 (main.go:33) LEAQ go.string."XJX"(SB), AX ;AX = &"XJX" 0x006b 00107 (main.go:33) MOVQ AX, (DI) ;StringHeader(DI.Name).Data = &"XJX" 0x006e 00110 (main.go:33) JMP 112 0x0070 00112 (main.go:32) PCDATA $0, $-1 0x0070 00112 (main.go:32) MOVQ ""..autotmp_3+32(SP), AX ;AX = &Student{} 0x0075 00117 (main.go:32) TESTB AL, (AX) 0x0077 00119 (main.go:34) MOVQ $15, 16(AX) ;DI.age = 15 0x007f 00127 (main.go:32) MOVQ ""..autotmp_3+32(SP), AX ;AX = &Student{}
- 先将
&type."".Student
放在(SP)
栈顶。 - 然后调用
runtime.newobject()
在堆中生成Student
对象并且返回地址。(SP)
栈顶的值即是newobject()
方法的入参。 - 后续将
name
和age
信息补充完整。因为name
为字符串,字符串的结构为StringHeader
,StringHeader
有Len
和Data
两个属性值分别为3
和XJX
字符串存放地址;age
则为int
基础类型,直接赋值即可。
我们用张图表示:
- 先将
-
把Student对象转化为Person interface指针
继续看代码:
0x008e 00142 (main.go:36) LEAQ go.itab.*"".Student,"".People(SB), CX;AX = *itab(go.itab.*"".Student,"".People) 0x0095 00149 (main.go:36) MOVQ CX, "".p+48(SP) ;p(48SP) = *itab(go.itab.*"".Student,"".People) 0x009a 00154 (main.go:36) MOVQ AX, "".p+56(SP) ;p(56SP) = &Student{}
经过上面几行汇编代码,成功的构造出了
itab
结构体以及iface
。如图:
-
调用interface方法
0x009f 00159 (main.go:37) MOVQ "".p+48(SP), AX ; AX = *itab(go.itab.*"".Student,"".People) 0x00a4 00164 (main.go:37) TESTB AL, (AX) 0x00a6 00166 (main.go:37) MOVQ 24(AX), AX ; AX = *(GetName) 0x00aa 00170 (main.go:37) MOVQ "".p+56(SP), CX ; CX = &Student{} 0x00af 00175 (main.go:37) MOVQ CX, (SP) ; 移动CX到栈顶 0x00b3 00179 (main.go:37) CALL AX ;call GetName func
取出
*itab(go.itab.*"".Student,"".People)
地址存放到AX寄存器,然后移动+24
字节,获取第一个函数的地址存入AX
,调用AX
即可。
结构体类型
来看下结构体类型的代码:
package main
import "fmt"
type People interface {
GetName() string
}
//go:noinline
func (s Student) GetName() string {
return s.name
}
type Student struct {
name string
age int
}
func main() {
var p People
var s Student = Student{
name: "XJX",
age: 15,
}
p = s
p.GetName()
}
这边就不详细说了,基本跟指针类型差不多,但有几点差别:
-
编译器发现变量只是临时变量时,没有调用
runtime.newobject()
,仅仅是将它的每个基本类型的字段生成好放在内存中。然后如果涉及到逃逸,则使用函数runtime.convTstring
函数将数据复制拷贝到堆区一份,runtime.convTstring
代码如下://iface.go func convTstring(val string) (x unsafe.Pointer) { if val == "" { x = unsafe.Pointer(&zeroVal[0]) } else { x = mallocgc(unsafe.Sizeof(val), stringType, true) *(*string)(x) = val } return }
在堆上分配了
stringStruct
并赋值,返回x
为对象地址。 -
初始化结构体后会进入类型转换的阶段,编译器会将
go.itab.""..Student,""..People
的地址和指向Student
结构体的指针作为参数一并传入runtime.convT2I
函数:func convT2I(tab *itab, elem unsafe.Pointer) (i iface) { t := tab._type x := mallocgc(t.size, t, true) typedmemmove(t, x, elem) i.tab = tab i.data = x return }
runtime.convT2I
会返回一个runtime.iface
,其中包含runtime.itab
指针和Student
变量。 -
在汇编调试过程中,发现一个现象,如果使用如下结构体方式赋值,编译器可能会进行优化,变量基本通过临时变量存储在栈中,也并不会存在类型转换调用
runtime.convT2I
函数,而跟指针类型类型直接在栈内构造完成,代码如下:var p People = Student{ name: "XJX", age: 15, } p.GetName()
类型断言
我们需要知道在golang
里面的函数参数可以为具体某一interface
或者interface{}
,这就是golang
的设计哲学,万物起源interface{}
,衍生出来specified interface
,接下来就是基于衍生的specified interface
衍生出来的现实生活中的实体–struct
。
有了这一步之后,就有一个问题需要被解决,就是有0到N个实现了某一个衍生接口的struct
,但是我又想对他们进行不一样的操作,这个时候就需要通过类型确定,这时候就需要类型断言。
在Go
语言的interface
中可以是任何类型,所以Go
给出了类型断言来判断某一时刻接口中所含有的类型,例如现在给出一个接口,名为InterfaceText
:
x := interfaceText.(T) //T是某一种类型,此方式断言失败会panic
or
x,err := interfaceText.(T)//T是某一种类型,此方式不会panic,错误信息会返回到err变量
上式是接口断言的一般形式,因为此方法不一定每次都可以完好运行,所以err的作用就是判断是否出错。所以一般接口断言常用以下写法:
if v,err:=InterfaceText.(T);err {//T是一种类型
possess(v)//处理v
return
}
如果转换合法,则v
为InterfaceText
转换为类型T的值,err
为ture
,反之err
为false
。
我们知道接口分为空接口和非空接口,我们从两类接口开始分析。
空接口
先从例子出发,看一段空接口代码:
package main
import "fmt"
type Student struct {
name string
}
type People interface {
GetName() string
}
func (s *Student) GetName() string {
return s.name
}
func main() {
var s interface{} = &Student{name: "XJX"}
v, ok := s.(int)
if ok {
fmt.Printf("%v\n", v)
}
switch s.(type) {
case People:
}
}
通过反编译查看源码:
go tool compile -N -l -S main.go
忽略其余代码我们从类型断言这里开始看起:
......
0x0070 00112 (main.go:17) LEAQ type.*"".Student(SB), CX ;把eface _type的地址放入CX
0x0077 00119 (main.go:17) MOVQ CX, "".s+120(SP) ;把CX存放值赋值给s+120(SP)
0x007c 00124 (main.go:17) MOVQ AX, "".s+128(SP) ;把AX存放值赋值给s+128(SP)
0x0084 00132 (main.go:18) MOVQ "".s+120(SP), AX ;把s+120(SP)存放寄存器AX,即eface _type地址
0x0089 00137 (main.go:18) MOVQ "".s+128(SP), CX ;把s+128(SP)存放寄存器CX,即eface data地址
0x0091 00145 (main.go:18) LEAQ type.int(SB), DX ;把int的类型type的地址放到 DX
0x0098 00152 (main.go:18) CMPQ DX, AX ;直接比较 AX DX的地址
......
我们知道类型元数据都存储在.rodata
区域内,获取空接口的_type
的地址位置和需要断言的类型的_type
位置进行比较,如果相同则代表类型一致。
下面看看空接口对于接口断言的汇编:
0x01b2 00434 (main.go:30) MOVL AX, ""..autotmp_14+68(SP)
0x01b6 00438 (main.go:31) MOVQ ""..autotmp_12+168(SP), AX
0x01be 00446 (main.go:31) MOVQ ""..autotmp_12+176(SP), CX
0x01c6 00454 (main.go:31) LEAQ type."".People(SB), DX ;获取People的类型元素存入DX
0x01cd 00461 (main.go:31) MOVQ DX, (SP) ;放入栈顶
0x01d1 00465 (main.go:31) MOVQ AX, 8(SP) ;放入eface
0x01d6 00470 (main.go:31) MOVQ CX, 16(SP)
0x01db 00475 (main.go:31) PCDATA $1, $0
0x01db 00475 (main.go:31) NOP
0x01e0 00480 (main.go:31) CALL runtime.assertE2I2(SB) ;判断发起调用
0x01e5 00485 (main.go:31) MOVBLZX 40(SP), AX ;bool值
0x01ea 00490 (main.go:31) MOVB AL, ""..autotmp_13+67(SP)
我们从汇编代码可以看出,空接口对于接口的断言,主要是函数 assertE2I2
:
func assertE2I2(inter *interfacetype, e eface) (r iface, b bool) {
//获取eface的类型元数据
t := e._type
if t == nil {
return
}
tab := getitab(inter, t, true)
if tab == nil {
return
}
r.tab = tab
r.data = e.data
b = true
return
}
原理也很简单,从空接口的eface
的_type
里取出类型元数据,再根据传入的interfacetype
去判断是否有对应的接口实现,成功则断言成功,否则返回nil和false。
非空接口
非空接口还是直接从例子开始看:
package main
import "fmt"
type People interface {
GetName() string
}
type Student struct {
name string
}
func (s *Student) GetName() string {
return s.name
}
func main() {
var p People = &Student{name: "XJX"}
v, ok := p.(People)
if ok {
fmt.Printf("%v\n", v)
}
v, ok = p.(*Student)
if ok {
fmt.Printf("%v\n", v)
}
}
执行命令
go tool compile -N -l -S main.go
来看下关键的代码汇编片段:
.......
0x00a1 00161 (main.go:26) MOVUPS X0, ""..autotmp_4+168(SP)
0x00a9 00169 (main.go:26) MOVQ "".p+136(SP), AX
0x00b1 00177 (main.go:26) MOVQ "".p+144(SP), CX
0x00b9 00185 (main.go:26) LEAQ type."".People(SB), DX ;获取type People的类型元数据地址到 DX
0x00c0 00192 (main.go:26) MOVQ DX, (SP) ;DX存入栈底
0x00c4 00196 (main.go:26) MOVQ AX, 8(SP) ;iface存入8(SP)
0x00c9 00201 (main.go:26) MOVQ CX, 16(SP)
0x00ce 00206 (main.go:26) PCDATA $1, $1
0x00ce 00206 (main.go:26) CALL runtime.assertI2I2(SB) ;调用方法进行类型断言
0x00d3 00211 (main.go:26) MOVBLZX 40(SP), AX ;返回参数 ok bool值
0x00d8 00216 (main.go:26) MOVQ 24(SP), CX ;新的iface
0x00dd 00221 (main.go:26) MOVQ 32(SP), DX ;新的iface
.......
主要看runtime.assertI2I2
函数:
func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) {
tab := i.tab //获取变量i的iface.tab
if tab == nil {
return
}
if tab.inter != inter { //如果i的face.tab跟inter不一致
tab = getitab(inter, tab._type, true) //使用inter类型与tab._type具体类型查询itab
if tab == nil { //如果查询不到,则表示断言失败
return
}
}
//断言成功 赋值
r.tab = tab
r.data = i.data
b = true
return
}
通过这样的方式我们就实现了接口类型断言。
我们再来看代码具体类型的断言这里和空接口的例子比较类似编译器也通过汇编代码优化进行了实现,没有去调用runtime函数:
.......
0x0207 00519 (main.go:30) MOVQ $0, ""..autotmp_3+88(SP)
0x0210 00528 (main.go:30) MOVQ "".p+144(SP), AX ;放入具体的AX
0x0218 00536 (main.go:30) LEAQ go.itab.*"".Student,"".People(SB), CX ;获取具体类型的itab的地址
0x021f 00543 (main.go:30) NOP
0x0220 00544 (main.go:30) CMPQ "".p+136(SP), CX ;比较两个itab的地址进行类型断言
0x0228 00552 (main.go:30) JEQ 559
0x022a 00554 (main.go:30) JMP 871
.......
简单补充下 p+144(SP)
放入的是p的具体值,p+136(SP)
放入的是p
的itab
,这个在前面的汇编函数已经实现,因为代码省略的原因特殊说明下。
最后用一张图来总结下类型断言:
![image-20220831170728307](https://xjxpicgo.oss-cn-hangzhou.aliyuncs.com/image-20220831170728307.png)
空接口 编译器会根据你要判断的是具体类型还是接口去判断是使用runtime.assertE2I2
还是直接使用汇编去实现。
非空接口也类似,也会根据要断言的是具体类型还是接口去判断调用runtime.assertI2I2
还是汇编直接比较。
runtime.assertE2I2
和runtime.assertI2I2
底层都调用了getitab去全局查找符合的itab
,这样就完成了类型的判断。
参考文档:
【新乐于心】https://www.zhihu.com/people/chen-qiang-song/posts
【draveness】 https://draveness.me/golang/
【幼麟实验室】https://space.bilibili.com/567195437