GO 语言 接口
interface
// 16 bytes on 64bit arch
type iface struct {
tab *itab
data unsafe.Pointer
}
// https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/runtime2.go#L143-L146
tab 是 itab 对象的地址, 用来描述interface的类型和它指向的数据类型的数据结构。
data 指向interface持有的具体的数据。
每次创建接口,都会发生堆上的内存分配。
itab 结构:
// layout of Itab known to compilers
// allocated in non-garbage-collected memory
// Needs to be in sync with
// ../cmd/compile/internal/gc/reflect.go:/^func.dumptypestructs.
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
// https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/runtime2.go#L648-L658
itab
内嵌了 _type
, _type
这个类型是runtime对任意GO语言类型的内部表示。
_type
类型 描述了一个“类型”的每一方面: 名字,特性,行为 等。
在这个例子中,_type
字段描述了interface所持有的值的类型,也就是data指针所指向的值的类型。
_type
结构
// Needs to be in sync with ../cmd/link/internal/ld/decodesym.go:/^func.commonsize,
// ../cmd/compile/internal/gc/reflect.go:/^func.dcommontype and
// ../reflect/type.go:/^type.rtype.
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldalign uint8
kind uint8
alg *typeAlg
// 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
}
// https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/type.go#L25-L43
nameOff
和 typeOff
类型是int32, 这两个值是链接器负责嵌入的,相对于可执行文件的元信息的偏移量。元信息会在运行期加载到runtime.moduledata
结构体中。
https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/symtab.go#L352-L393
runtime提供了一些helper函数,这些函数能够帮助找到moduledata的偏移量,比如
func resolveNameOff(ptrInModule unsafe.Pointer, off nameOff) name {}
func resolveTypeOff(ptrInModule unsafe.Pointer, off typeOff) *_type {}
假设t是_type的话,只要调用 resolveTypeOff(t, t.ptrToThis)
就可以返回t的一份拷贝了。
interfacetype
结构体
// 80 bytes on a 64bit arch
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
type imethod struct {
name nameOff
ityp typeOff
}
interfacetype
是 对于 _type
的一种包装,在其顶部空间还包装了额外的interface
相关的元信息。
展开iface结构
type iface struct {
tab *struct { // `itab`
inter *struct {
typ struct {
size uintptr
ptrdata uintptr
hash uint32
tflag tflag
align uint8
fieldalign uint8
kind uint8
alg *typeAlg
gcdata *byte
str nameOff
ptrToThis typeOff
}
pkgpath name
mhdr []imethod
}
_type *struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldalign uint8
kind uint8
alg *typeAlg
gcdata *byte
str nameOff
ptrToThis typeOff
}
hash uint32
_ [4]byte
fun [1]uintptr
}
data unsafe.Pointer
}
创建接口:
iface.go
type Mather interface {
Add(a, b int32) int32
Sub(a, b int64) int64
}
type Adder struct { id int32 }
//go:noinline
func (adder Adder) Add(a, b int32) int32 { return a + b }
//go:noinline
func (adder Adder) Sub(a, b int64) int64 { return a - b }
func main() {
m := Mather(Adder{id: 6754})
m.Add(10, 32)
}
剩余部分将演示一个持有T类型内容I
类型的interface,即<I, T>
.
比如Mather(Adder{id: 6754})
就实例化了一个 iface<Mather, Adder>
;; part 1: allocate the receiver
0x001d MOVL $6754, ""..autotmp_1+36(SP)
;; part 2: set up the itab
0x0025 LEAQ go.itab."".Adder,"".Mather(SB), AX
0x002c MOVQ AX, (SP)
;; part 3: set up the data
0x0030 LEAQ ""..autotmp_1+36(SP), AX
0x0035 MOVQ AX, 8(SP)
0x003a CALL runtime.convT2I32(SB)
0x003f MOVQ 16(SP), AX
0x0044 MOVQ 24(SP), CX
Part1:
MOVL $6754, ""..autotmp_1+36(SP)
实例化Adder空间,Adder即Receiver.
Part2:
LEAQ go.itab."".Adder,"".Mather(SB), AX
MOVQ AX, (SP)
看起来编译器已经创建了必要的itab来表示我们的iface<Mather, Adder>
interface, 并以go.itab."".Adder,"".Mather(SB)供我们使用。
我们可以用伪代码来代替上面几行代码
tab := getSymAddr(`go.itab.main.Adder,main.Mather`).(*itab)
深入研究一下 go.itab."".Adder,"".Mather
符号。
编译器-S 查看汇编
$ GOOS=linux GOARCH=amd64 go tool compile -S iface.go | grep -A 7 '^go.itab."".Adder,"".Mather'
go.itab."".Adder,"".Mather SRODATA dupok size=40
0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x0010 8a 3d 5f 61 00 00 00 00 00 00 00 00 00 00 00 00 .=_a............
0x0020 00 00 00 00 00 00 00 00 ........
rel 0+8 t=1 type."".Mather+0
rel 8+8 t=1 type."".Adder+0
rel 24+8 t=1 "".(*Adder).Add+0
rel 32+8 t=1 "".(*Adder).Sub+0
第一句声明了符号和属性
go.itab."".Adder,"".Mather SRODATA dupok size=40
由于我们看的是编译器生成的间接目标文件,符号名还没有把package名字填充上。除此之外,我们的得到的是一个40字节的全局对象符号,该符号被存到二进制文件的.rodata段中。
40个字节的数据为
0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x0010 8a 3d 5f 61 00 00 00 00 00 00 00 00 00 00 00 00 .=_a............
0x0020 00 00 00 00 00 00 00 00 ........
type itab struct { // 40 bytes on a 64bit arch
inter *interfacetype // offset 0x00 ($00)
_type *_type // offset 0x08 ($08)
hash uint32 // offset 0x10 ($16)
_ [4]byte // offset 0x14 ($20)
fun [1]uintptr // offset 0x18 ($24)
// offset 0x20 ($32)
}
可以看到 8a 3d 5f 61
与 hash uint32
相对应。
也就是说 main.Adder的hash值已经在我们的目标文件中了。
同时,还列出了提供给链接器重定向的相关指令:
rel 0+8 t=1 type."".Mather+0
rel 8+8 t=1 type."".Adder+0
rel 24+8 t=1 "".(*Adder).Add+0
rel 32+8 t=1 "".(*Adder).Sub+0
rel 0+8 t=1 type."".Mather+0
告诉链接器,将内容的前8个字节填充为 全局目标符号 type."".Mather
的地址。
rel 8+8 t=1 type."".Adder+0
告诉链接器,将内容的8~16个字节填充为 全局目标符号 type."".Adder
的地址。
一旦链接器完成了它的工作,执行完了这些指令, 40 字节后的序列化的 itab
就完成了。
我们再看的这些代码类似以下伪代码
tab := getSymAddr(`go.itab.main.Adder,main.Main.Mather`).(*itab)
tab.inter = getSymAddr(`type.main.Mather`).(*interfacetype)
tab._type = getSymAddr(`type.main.Adder`).(*_type)
tab.fun[0] = getSymAddr(`main.(*Adder).Add`).(uintptr)
tab.fun[1] = getSymAddr(`main.(*Adder).Sub`).(uintptr)
目前以前得到了一个完整可用的itab
, 如果能再有一些相关的数据塞进去,就能得到一个完成的Interface了。
Part3: 分配数据
0x0030 LEAQ ""..autotmp_1+36(SP), AX // 36(SP) 存储着Adder对象
0x0035 MOVQ AX, 8(SP) // 8(SP)存储该对象的地址
0x003a CALL runtime.convT2I32(SB)
0x003f MOVQ 16(SP), AX
0x0044 MOVQ 24(SP), CX
Part1 中说过, 栈顶 (SP) 保存这 go.itab."".Adder."".Mather 的地址。同时Part2 我们在 36(SP)中存储一个十进制常量 $6754, 我们用 8(SP) 来将栈顶下方的该变量load到寄存器中。
所以 runtime.convT2I32
的参数1为 go.itab."".Adder."".Mather
的地址, 参数2为 &Adder
看函数地址 convT2I32
func convT2I32(tab *itab, elem unsafe.Pointer) (i iface) {
t := tab._type
/* ...omitted debug stuff... */
var x unsafe.Pointer
if *(*uint32)(elem) == 0 {
x = unsafe.Pointer(&zeroVal[0])
} else {
x = mallocgc(4, t, false)
*(*uint32)(x) = *(*uint32)(elem)
}
i.tab = tab
i.data = x
return
}
runtime.convT2I32
这个函数做了4件事
- 创建了一个iface的结构体 i.
- 将我们刚给i.tab 赋的值赋予了itab指针。
- 它在堆上分配了一个i.tab._type的新对象 i.tab._type, 然后将第二个参数elem指向的参数拷贝到这个新对象上。
- 返回interface.
现在我们已经完成了下面的这些工作(伪代码)
tab := getSymAddr(`go.itab.main.Adder,main.Mather`).(*itab)
elem := getSymAddr(`""..autotmp_1+36(SP)`).(*int32)
i := runtime.convTI32(tab, unsafe.Pointer(elem))
assert(i.tab == tab)
assert(*(*int32)(i.data) == 6754) // same value..
assert((*int32)(i.data) != elem) // ..but different (al)locations!
这些代码都是 m := Mather(Adder{id: 6754})
这一行代码生成的。
最终,我们得到了完成的可以工作的interface.
参考:
https://github.com/go-internals-cn/go-internals/blob/master/chapter2_interfaces/README.md#%E8%A7%A3%E6%9E%84%E6%8E%A5%E5%8F%A3