本文是读了左大《go语言设计与实现》接口章节笔记以及实际做实验的记录,实验环境为
go version go1.14.2 windows/amd64
golang中接口类型分为两种,一种是不带任何方法的空接口,一种是带一组方法的接口,它们对应的数据结构如下
type iface struct { // 带一组方法的接口
tab *itab
data unsafe.Pointer
}
type eface struct { // 不带方法的接口
_type *_type
data unsafe.pointer
}
type itab struct { // itab类型
inter *interfacetype// 接口类型元数据
_type *_type // 值具体类型元数据
hash uint32 // _type.hash的拷贝,用来做类型断言
_ [4]byte
fun [1]uintptr // 虚函数表,存储一组函数指针,虽然申明为固定数组,但使用时会通过原始指针获取其中的数据
}
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte
str nameOff
ptrToThis typeOff
}
类型转换
非空接口iface
结构体实现接口
func main() {
var i I = Cat{Name: "world", Age: 20}
i.i()
}
type I interface {
i() int
}
type Cat struct {
Name string
Age uint64
}
func (c Cat) i() int {
var a int = 1
return a
}
运行go tool compile -N -S -l main.go后得到汇编如下
// main函数分配栈空间
0x001a 00026 (main.go:3) SUBQ $80, SP
0x001e 00030 (main.go:3) MOVQ BP, 72(SP)
0x0023 00035 (main.go:3) LEAQ 72(SP), BP
// 初始化Cat结构体
0x0028 00040 (main.go:4) MOVQ $0, ""..autotmp_1+48(SP)
0x0031 00049 (main.go:4) XORPS X0, X0
0x0034 00052 (main.go:4) MOVUPS X0, ""..autotmp_1+56(SP)
0x0039 00057 (main.go:4) LEAQ go.string."world"(SB), AX
0x0040 00064 (main.go:4) MOVQ AX, ""..autotmp_1+48(SP)
0x0045 00069 (main.go:4) MOVQ $5, ""..autotmp_1+56(SP)
0x004e 00078 (main.go:4) MOVQ $20, ""..autotmp_1+64(SP)
// 将初始化好的Cat结构体装换成接口I
0x0057 00087 (main.go:4) LEAQ go.itab."".Cat,"".I(SB), AX
0x005e 00094 (main.go:4) MOVQ AX, (SP)
0x0062 00098 (main.go:4) LEAQ ""..autotmp_1+48(SP), AX
0x0067 00103 (main.go:4) MOVQ AX, 8(SP)
0x006c 00108 (main.go:4) CALL runtime.convT2I(SB)
0x0071 00113 (main.go:4) MOVQ 16(SP), AX
0x0076 00118 (main.go:4) MOVQ 24(SP), CX
0x007b 00123 (main.go:4) MOVQ AX, "".i+32(SP)
0x0080 00128 (main.go:4) MOVQ CX, "".i+40(SP)
// 调用函数
0x0085 00133 (main.go:5) MOVQ "".i+32(SP), AX
0x008a 00138 (main.go:5) TESTB AL, (AX)
0x008c 00140 (main.go:5) MOVQ 24(AX), AX
0x0090 00144 (main.go:5) MOVQ "".i+40(SP), CX
0x0095 00149 (main.go:5) MOVQ CX, (SP)
0x0099 00153 (main.go:5) CALL AX
0x009b 00155 (main.go:6) MOVQ 72(SP), BP
0x00a0 00160 (main.go:6) ADDQ $80, SP
0x00a4 00164 (main.go:6) RET
其中convT2I函数代码如下,可以看到传入了tab和真实数据的地址elem,分别对应了iface中的tab和data,返回了一个iface类型,该函数的作用就是按照itab记录的真实类型在堆中分配内存空间x,然后将elem指向的真实数据复制到x中,然后将传入的tab给iface就返回
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
}
在汇编代码第24行之后,main函数栈空间如下图所示sp+48 ~ sp+72 这段空间存储的是代码中 Cat{Name: "world", Age: 20}sp+32 ~ sp+40 这段空间存储的就是i变量,i 变量是一个iface结构体sp+16 ~ sp+32 这段空间是调用convT2I函数的返回值,也是一个iface,其中sp+8这个位置记录的是&Cat与另外两个不同,它存储的是原始Cat结构体的地址 ,与其他两个不同,其他两个是指向堆中的Cat结构体,这个是在调用convT2I函数时创建的sp ~ sp+16这段空间存储的是传递给convT2I函数的参数,分别对应*itab和unsafe.Pointer
在汇编代码32行调用函数之前,main栈空间如下图所示,其中sp位置变成了堆中Cat结构体的地址,它是调用i函数时的参数,用以在调用i函数
这里有一点困惑的时,我们采用的是结构体实现的接口,也是使用结构体装换成接口,但是调用函数时,传递的参数明显不是结构体,而是一个结构体的地址,即iface中的data;如果直接用Cat结构体调用i方法(如var cat = Cat{}; cat.i()),而不是通过接口的方式调用,则可以发现传递的参数是Cat结构体,即上图中sp ~ sp+24之间数据依次为 &("world"),5,20
对于上述问题,是为了确定栈帧大小,因为实现接口的结构体各种各样,如果使用值接收器方法,那需要在栈上放置整个接收者的数据,在分配栈空间时也需要综合考虑所有可能的结构体的大小,不太好分配栈帧大小,因此直接使用指针。推荐幼麟工作室的观点链接:www.bilibili.com/video/BV1hv…
结构体指针实现接口
func main() {
var i I = &Cat{Name: "world", Age: 20}
i.i()
}
type I interface {
i() int
}
type Cat struct {
Name string
Age uint64
}
func (c *Cat) i() int {
var a int = 1
return a
}
运行go tool compile -S -N -l main.go后,生成汇编语言如下,删减了一些无用代码
// main函数分配栈空间
0x001a 00026 (main.go:3) SUBQ $56, SP
0x001e 00030 (main.go:3) MOVQ BP, 48(SP)
0x0023 00035 (main.go:3) LEAQ 48(SP), BP
// 初始化Cat结构体,并取其地址
0x0028 00040 (main.go:4) LEAQ type."".Cat(SB), AX
0x002f 00047 (main.go:4) MOVQ AX, (SP)
0x0033 00051 (main.go:4) CALL runtime.newobject(SB)
0x0038 00056 (main.go:4) MOVQ 8(SP), DI
0x003d 00061 (main.go:4) MOVQ DI, ""..autotmp_2+16(SP)
0x0042 00066 (main.go:4) MOVQ $5, 8(DI)
0x0055 00085 (main.go:4) LEAQ go.string."world"(SB), AX
0x005c 00092 (main.go:4) MOVQ AX, (DI)
0x0061 00097 (main.go:4) MOVQ ""..autotmp_2+16(SP), AX
0x0066 00102 (main.go:4) TESTB AL, (AX)
0x0068 00104 (main.go:4) MOVQ $20, 16(AX)
// 初始化i变量,i类型为接口I
0x0070 00112 (main.go:4) MOVQ ""..autotmp_2+16(SP), AX
0x0075 00117 (main.go:4) MOVQ AX, ""..autotmp_1+24(SP)
0x007a 00122 (main.go:4) LEAQ go.itab.*"".Cat,"".I(SB), CX
0x0081 00129 (main.go:4) MOVQ CX, "".i+32(SP)
0x0086 00134 (main.go:4) MOVQ AX, "".i+40(SP)
// 调用i.i()方法
0x008b 00139 (main.go:5) MOVQ "".i+32(SP), AX
0x0090 00144 (main.go:5) TESTB AL, (AX)
0x0092 00146 (main.go:5) MOVQ 24(AX), AX
0x0096 00150 (main.go:5) MOVQ "".i+40(SP), CX
0x009b 00155 (main.go:5) MOVQ CX, (SP)
0x009f 00159 (main.go:5) CALL AX
// 返回
0x00a1 00161 (main.go:6) MOVQ 48(SP), BP
0x00a6 00166 (main.go:6) ADDQ $56, SP
0x00aa 00170 (main.go:6) RET
runtime.newobject函数是为指定类型分配内存空间,并返回分配空间的地址,其源代码如下所示
func newobject(typ *_type) unsafe.Pointer {
return mallocgc(typ.size, typ, true)
}
初始化i变量完成后,即执行完第24行汇编代码之后,main函数栈空间数据如下所示sp ~ sp+8 这个空间保存的是Cat类型元数据的地址,它作为runtime.newobject的参数sp+8 ~ sp+16 是堆中的Cat结构体的地址,它是runtime.newobject的返回值sp+16 ~ sp+32 两段空间都是堆中Cat结构体的地址,没啥用,属于临时存放sp+32 ~ sp+48 中存放的是变量i,其中包括itab和data数据
在调用函数时,即第31行汇编代码,main函数栈空间如下图所示
与上图相比,只在sp ~ sp+8 之间有变化,它作为传递给i函数的参数,指向堆中Cat结构体
空接口eface
实验代码如下
func main() {
var i interface{} = Cat{Name: "Hello", Age: 200}
print(i)
}
type Cat struct {
Name string
Age int
}
编译后得到下列代码,之前的初始化代码与前面差不多,只截取关键代码如下
0x006e 00110 (main.go:4) LEAQ type."".Cat(SB), AX
0x0075 00117 (main.go:4) MOVQ AX, "".i+16(SP)
0x007a 00122 (main.go:4) LEAQ ""..autotmp_2+32(SP), AX
0x007f 00127 (main.go:4) MOVQ AX, "".i+24(SP)
代码很简单将类型信息的地址放到sp+16的位置,实际值地址保存在sp+24的位置,不做过多赘述了
类型断言
实验代码为
package main
func main() {
var cat = &Cat{Name: "Hello", Age: 100}
var i I = cat
switch i.(type) {
case *Cat:
print("*Cat")
case Cat:
print("Cat")
}
return
}
type I interface {
i()
}
type Cat struct {
Name string
Age uint64
}
func (c Cat) i() {
c.Name = "World"
c.Age = 200
}
汇编语言如下,总的来说,就是按照每个分支对比,看进入哪个分支,对比流程就是对比接口中保存的类型hash值与case分支中的类型hash值是否相等,然后还要对比go.itab的地址与接口中的*itab是否相等,如果相等则进入对应的分支(注:这段代码使用的时go1.18.2 windows/amd64)
// 分配栈空间
0x000a 00010 (main.go:3) SUBQ $112, SP
0x000e 00014 (main.go:3) MOVQ BP, 104(SP)
0x0013 00019 (main.go:3) LEAQ 104(SP), BP
// 初始化Cat结构体
0x0018 00024 (main.go:4) MOVQ $0, ""..autotmp_4+80(SP)
0x0021 00033 (main.go:4) MOVUPS X15, ""..autotmp_4+88(SP)
0x0027 00039 (main.go:4) LEAQ ""..autotmp_4+80(SP), AX
0x002c 00044 (main.go:4) MOVQ AX, ""..autotmp_3+32(SP)
0x0031 00049 (main.go:4) TESTB AL, (AX)
0x0033 00051 (main.go:4) MOVQ $5, ""..autotmp_4+88(SP)
0x003c 00060 (main.go:4) LEAQ go.string."Hello"(SB), CX
0x0043 00067 (main.go:4) MOVQ CX, ""..autotmp_4+80(SP)
0x0048 00072 (main.go:4) TESTB AL, (AX)
0x004a 00074 (main.go:4) MOVQ $100, ""..autotmp_4+96(SP)
0x0053 00083 (main.go:4) MOVQ AX, "".cat+24(SP)
// Cat结构体指针转为接口I
0x0058 00088 (main.go:5) MOVQ AX, ""..autotmp_2+40(SP)
0x005d 00093 (main.go:5) LEAQ go.itab.*"".Cat,"".I(SB), CX
0x0064 00100 (main.go:5) MOVQ CX, "".i+48(SP)
0x0069 00105 (main.go:5) MOVQ AX, "".i+56(SP)
// 下面是switch的相关代码
0x006e 00110 (main.go:6) MOVQ "".i+48(SP), AX
0x0073 00115 (main.go:6) MOVQ "".i+56(SP), CX
0x0078 00120 (main.go:6) MOVQ AX, ""..autotmp_5+64(SP)
0x007d 00125 (main.go:6) MOVQ CX, ""..autotmp_5+72(SP)
0x0082 00130 (main.go:6) TESTQ AX, AX
// 1. 首先对比i变量是否为nil,如果i==nil,则跳转到结束位置
0x0085 00133 (main.go:6) JNE 140 // 如果i不是nil
0x0087 00135 (main.go:6) JMP 326 // 这里判断i是否为nil,如果是,则跳转到00326执行,直接结束
0x008c 00140 (main.go:6) MOVL 16(AX), AX // 这里是取出itab中的hash值到ax
0x008f 00143 (main.go:6) MOVL AX, ""..autotmp_7+20(SP)
// 2. 下面是Case Cat分支,看接口i具体值是否为Cat类型
0x0093 00147 (main.go:6) CMPL AX, $284731410 // 对比hash值与Cat类型的hash值,这个hash值是在编译的时候计算的,存储在只读存储区中
0x0098 00152 (main.go:6) JEQ 156 // 如果相等,往下执行
0x009a 00154 (main.go:6) JMP 233 // 如果不相等,跳转到00233指令执行
0x009c 00156 (main.go:9) MOVQ ""..autotmp_5+64(SP), AX // 取出变量i中*itab的值放到AX
0x00a1 00161 (main.go:9) LEAQ go.itab."".Cat,"".I(SB), CX // 取出go.itab."".Cat,"".I的地址到CX,这个也是在编译的时候生成的,存储在只读存储区中
0x00a8 00168 (main.go:9) CMPQ AX, CX // 将i中*itab与go.itab."".Cat做比较
0x00ab 00171 (main.go:9) JEQ 175 // 如果相等,往下执行
0x00ad 00173 (main.go:9) JMP 182 // 不相等则跳转到00182指令
0x00af 00175 (main.go:9) MOVL $1, AX // 这里往AX中放了个1,可以认为是标志位,说明00171代码对比结果为相等
0x00b4 00180 (main.go:9) JMP 186
0x00b6 00182 (main.go:9) XORL AX, AX // 这里将AX清空了,是00171代码对比结果为不相等的清空
0x00b8 00184 (main.go:9) JMP 186
0x00ba 00186 (main.go:9) MOVB AL, ""..autotmp_6+19(SP)
0x00be 00190 (main.go:9) NOP
0x00c0 00192 (main.go:9) TESTB AL, AL // 这里旧用到了前面的标志位,用变量flag表示
0x00c2 00194 (main.go:9) JNE 198 // 如果flag != 0,跳转到00198
0x00c4 00196 (main.go:9) JMP 231 // 如果flag == 0, 跳转到00231
0x00c6 00198 (main.go:9) JMP 200 // 继续往下执行
// 这里是如果i中保存的是Cat类型,调用print函数,输出"Cat"字符串
0x00c8 00200 (main.go:10) CALL runtime.printlock(SB)
0x00cd 00205 (main.go:10) LEAQ go.string."Cat"(SB), AX
0x00d4 00212 (main.go:10) MOVL $3, BX
0x00d9 00217 (main.go:10) CALL runtime.printstring(SB)
0x00de 00222 (main.go:10) NOP
0x00e0 00224 (main.go:10) CALL runtime.printunlock(SB)
0x00e5 00229 (main.go:6) JMP 328 // 跳转到返回地址,结束执行
0x00e7 00231 (main.go:6) JMP 324 // 这里是flag == 0的,跳转到00324,其实也是跳转到返回地址
// 3. 下面是Case *Cat分支,看接口i具体值是否为*Cat类型,流程与上面类似
// 首先判断接口中保存的类型hash值是否与*Cat类型hash值相等
// 然后判断接口中*itab指针指向的地址与编译时生成的go.itab.*"".Cat,"".I相等
0x00e9 00233 (main.go:6) CMPL AX, $593696792
0x00ee 00238 (main.go:6) JEQ 242
0x00f0 00240 (main.go:6) JMP 320
0x00f2 00242 (main.go:7) MOVQ ""..autotmp_5+64(SP), AX
0x00f7 00247 (main.go:7) LEAQ go.itab.*"".Cat,"".I(SB), CX
0x00fe 00254 (main.go:7) NOP
0x0100 00256 (main.go:7) CMPQ AX, CX
0x0103 00259 (main.go:7) JEQ 263
0x0105 00261 (main.go:7) JMP 270
0x0107 00263 (main.go:7) MOVL $1, AX
0x010c 00268 (main.go:7) JMP 274
0x010e 00270 (main.go:7) XORL AX, AX
0x0110 00272 (main.go:7) JMP 274
0x0112 00274 (main.go:7) MOVB AL, ""..autotmp_6+19(SP)
0x0116 00278 (main.go:7) TESTB AL, AL
0x0118 00280 (main.go:7) JNE 284
0x011a 00282 (main.go:7) JMP 317
0x011c 00284 (main.go:7) JMP 286
0x011e 00286 (main.go:8) NOP
0x0120 00288 (main.go:8) CALL runtime.printlock(SB)
0x0125 00293 (main.go:8) LEAQ go.string."*Cat"(SB), AX
0x012c 00300 (main.go:8) MOVL $4, BX
0x0131 00305 (main.go:8) CALL runtime.printstring(SB)
0x0136 00310 (main.go:8) CALL runtime.printunlock(SB)
0x013b 00315 (main.go:6) JMP 328
// 后面都是返回地址
0x013d 00317 (main.go:6) JMP 322
0x013f 00319 (main.go:6) NOP
0x0140 00320 (main.go:6) JMP 322
0x0142 00322 (main.go:6) JMP 324
0x0144 00324 (main.go:6) JMP 328
0x0146 00326 (main.go:6) JMP 328
0x0148 00328 (main.go:12) MOVQ 104(SP), BP
0x014d 00333 (main.go:12) ADDQ $112, SP
0x0151 00337 (main.go:12) RET
动态派发
理解了接口的数据结构之后,动态派发也很简单了。直接根据其中的itab的func函数列表中选择合适的函数即可,接收者即为iface中的data数据itab结构是在编译器生成的,对于每个接口的每个实现者都会有一个固定的itab,以下面的代码为例,Cat和Dog都实现了接口I
func main() {
var i I = Cat{Name: "hello", Age: 12}
i.a()
i = Dog{}
i.b()
}
type I interface {
a()
b()
}
type Cat struct {
Name string
Age int
}
func (c Cat) a() {
}
func (c Cat) b() {
}
type Dog struct {
Name string
Age int
}
func (c Dog) a() {
}
func (c Dog) b() {
}
在生成的汇编代码中搜索go.itab,就能找到相关的itab结构。注意,一定要在代码中有将Cat和Dog转为接口I的指令,否则生成的汇编代码不会生成这些itab结构
go.itab."".Dog,"".I SRODATA dupok size=40 // Dog的itab结构
0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x0010 bf 36 61 92 00 00 00 00 00 00 00 00 00 00 00 00 .6a.............
0x0020 00 00 00 00 00 00 00 00 ........
rel 0+8 t=1 type."".I+0
rel 8+8 t=1 type."".Dog+0
rel 24+8 t=1 "".(*Dog).a+0 // (*Dog).a的函数地址
rel 32+8 t=1 "".(*Dog).b+0 // (*Dog).b的函数地址
go.itab."".Cat,"".I SRODATA dupok size=40 // Cat的itab结构
0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x0010 12 a8 f8 10 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x0020 00 00 00 00 00 00 00 00 ........
rel 0+8 t=1 type."".I+0
rel 8+8 t=1 type."".Cat+0
rel 24+8 t=1 "".(*Cat).a+0
rel 32+8 t=1 "".(*Cat).b+0
所以在动态派发的时候,只需要根据iface.itab即可找到对应的函数即可
结构体和指针
左大在书中陈述了为什么结构体指针转为接口,再调用值接收器的方法是不被允许的原因,个人不是很理解,找了一些资料,以下是相关链接及其截图
存储在接口中的具体值是不可寻址的
第一个观点首先出自go wiki中关于方法集的文档,链接:github.com/golang/go/w…
至于为什么要求这个具体值是不可寻址的,StackOverflow上有位大佬举了个例子:stackoverflow.com/a/48874650/…
语言规范问题
首先得说方法集这个概念:go.dev/ref/spec#Me…
T类型的方法集包含所有使用T作为接收器定义的方法*T类型的方法集包含所有使用T或*T作为接收器定义的方法- 接口类型的方法集合,是定义在接口中所有类型集合的交集
其中第三点是针对go1.18泛型出现后的补充,这里不做过多描述
而为什么是*T的方法集包含T的方法集合,在Go FAQ中是这么说的:go.dev/doc/faq#dif…
接口中保存了*T,通过*T可以解引用获得T,如果反过来,那没有一个安全的方式获取T的指针*T,如果那样做了,将会允许方法修改保存在接口中值的内容
其他观点
最后一个观点是来自于左大同一个章节的评论中其他大佬的回复
个人认为该观点比较合理,在结构体变量转换成接口后,接口中保存的是结构体变量的拷贝值,而不是原来的值,而我们调用指针接收器的函数肯定是期望使用原值的指针,但是靠接口是无法获得原值的指针的,所以直接让其编译不通过
我认为这个是可以实现的,只是golang官方没有这样实现罢了,估计原因就是评论中所说的explicit is better than implicit,如果通过编译器隐式创建一个新的指针指向原值,那对GC也是会有一定影响的,对开发人员会产生一定的困惑