iface

结构

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
type iface struct {
tab *itab
data unsafe.Pointer
}

// 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 // 4字节填充,与上面的4字节hash凑成8字节,与n内存对齐相关
// itab末尾是实现方法的引用,如果多余1个,则其余方法引用紧跟itab内存之后分配
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

type interfacetype struct {
typ _type // 接口类型信息
pkgpath name
mhdr []imethod // 接口声明的方法
}


// 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 // 类型hash值
tflag tflag // 类型相关一些flag,可以在反射包中使用
align uint8 // 内存对齐
fieldalign uint8 // 字段对齐
kind uint8 // 类型kind
alg *typeAlg // 类型的hash和equal方法
// 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 // 存储gc相关的信息
str nameOff // offset of name
ptrToThis typeOff
}

这里的_type是最基本的类型信息,而实际我们声明的类型还有包含字段、方法等信息,查看下面代码,可以看到_type只是实际类型结构的一部分

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
}

func (t *_type) uncommon() *uncommontype {
if t.tflag&tflagUncommon == 0 {
return nil
}
switch t.kind & kindMask {
case kindStruct:
type u struct {
structtype
u uncommontype
}
return &(*u)(unsafe.Pointer(t)).u
case kindPtr:
type u struct {
ptrtype
u uncommontype
}
return &(*u)(unsafe.Pointer(t)).u
case kindFunc:
type u struct {
functype
u uncommontype
}
return &(*u)(unsafe.Pointer(t)).u
case kindSlice:
type u struct {
slicetype
u uncommontype
}
return &(*u)(unsafe.Pointer(t)).u
case kindArray:
type u struct {
arraytype
u uncommontype
}
return &(*u)(unsafe.Pointer(t)).u
case kindChan:
type u struct {
chantype
u uncommontype
}
return &(*u)(unsafe.Pointer(t)).u
case kindMap:
type u struct {
maptype
u uncommontype
}
return &(*u)(unsafe.Pointer(t)).u
case kindInterface:
type u struct {
interfacetype
u uncommontype
}
return &(*u)(unsafe.Pointer(t)).u
default:
type u struct {
_type
u uncommontype
}
return &(*u)(unsafe.Pointer(t)).u
}
}

通过汇编看iface

1
2
3
4
5
6
7
8
9
10
func main() {
var r io.Reader = Arr{}
r.Read(nil)
}

type Arr []byte

func (Arr) Read(n []byte) (int, error) {
return 0, nil
}
1
$ go tool compile -S main.go > main.s
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
29
30
31
32
33
34
35
	0x0000 00000 (test.go:7)	TEXT	"".main(SB), $88-0
...
0x0024 00036 (test.go:8) LEAQ type.[0]uint8(SB), AX // newobject方法参数
0x002b 00043 (test.go:8) MOVQ AX, (SP)
0x002f 00047 (test.go:8) CALL runtime.newobject(SB)
0x0034 00052 (test.go:8) MOVQ 8(SP), AX // 这里把返回的指针保存到AX
0x0039 00057 (test.go:8) MOVQ AX, ""..autotmp_1+56(SP) // Arr对象
0x003e 00062 (test.go:8) XORPS X0, X0
0x0041 00065 (test.go:8) MOVUPS X0, ""..autotmp_1+64(SP)
0x0046 00070 (test.go:8) LEAQ go.itab."".Arr,io.Reader(SB), AX // itab
0x004d 00077 (test.go:8) MOVQ AX, (SP)
0x0051 00081 (test.go:8) LEAQ ""..autotmp_1+56(SP), AX // Arr对象指针
0x0056 00086 (test.go:8) MOVQ AX, 8(SP)
0x005b 00091 (test.go:8) CALL runtime.convT2Islice(SB) // 生成iface
0x0060 00096 (test.go:8) MOVQ 24(SP), AX // 接口的data字段
0x0065 00101 (test.go:8) MOVQ 16(SP), CX // 接口的tab字段,即itab表
0x006a 00106 (test.go:9) MOVQ 24(CX), CX // itab的24~32为实际方法Read地址
// 110~127构造一个 Arr{0 0 data}结构,来调用方法Read
// 实际上方法的接收者就是方法第一个参数
0x006e 00110 (test.go:9) MOVQ $0, 8(SP) // 0
0x0077 00119 (test.go:9) XORPS X0, X0
0x007a 00122 (test.go:9) MOVUPS X0, 16(SP) // 0
0x007f 00127 (test.go:9) MOVQ AX, (SP) // data
0x0083 00131 (test.go:9) CALL CX // 调用Read方法
0x008e 00142 (test.go:10) RET

// 有些itab在编译期间可以自动生成
// 可能在a文件声明了Arr,b文件中声明了接口Reader,在C包文件c和D包文件d都用到了Arr初始化接口Reader,
// 则会在c文件和d文件都生成这个itab表,因此声明为dupok,由链接器任意选择一个
go.itab."".Arr,io.Reader SRODATA dupok size=32
0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x0010 d6 ed 1d 4e 00 00 00 00 00 00 00 00 00 00 00 00 ...N............
rel 0+8 t=1 type.io.Reader+0
rel 8+8 t=1 type."".Arr+0
rel 24+8 t=1 "".(*Arr).Read+0 // 这里rel告诉链接器要将24~32的符号引用替换成方法的逻辑地址

可以看到,实现接口的方法引用列表会保存在itab末尾,调用时,需要先计算具体调用函数的偏移获取实际方法引用

上面生成接口值的时候,调用了runtime.convT2Islice方法,其实在runtime.iface.go中声明了一系列runtime.convT2IXXX的方法,表示将XXX类型的值转换成一个接口值,因为这里的例子中,ArrKindslice,因此调用的是runtime.convT2Islice这个方法。

下面我们来看一下convT2IsliceconvT2I这两个方法的实现。

先来看convT2Islice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 参数elem实际上是一个slice的指针
// 将一个实际类型的值转换成接口值这种情况,itab由编译器自动生成
func convT2Islice(tab *itab, elem unsafe.Pointer) (i iface) {
t := tab._type //这里的_type就是该接口背后的真实数据类型
if raceenabled {
raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2Islice))
}
if msanenabled {
msanread(elem, t.size)
}
var x unsafe.Pointer
// 如果切片的底层数组是nil
if v := *(*slice)(elem); uintptr(v.array) == 0 {
x = unsafe.Pointer(&zeroVal[0])
} else {
x = mallocgc(t.size, t, true) // 分配一个slice
*(*slice)(x) = *(*slice)(elem) // 赋值
}
i.tab = tab
i.data = x // 返回的接口值中的data指向的内存是elem的拷贝
return
}

然后是convT2I,这个是比较通用的转换方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 这里的elem是要转换成接口值的实际值的指针
// 由实际类型转换成接口类型的情况,itab由编译器自动生成
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
t := tab._type
if raceenabled {
raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
}
if msanenabled {
msanread(elem, t.size)
}
x := mallocgc(t.size, t, true) // 这里根据*elem的大小分配一块内存
typedmemmove(t, x, elem) // 内存拷贝
i.tab = tab
i.data = x
return
}

当我们把一个非指针值赋给一个接口类型的变量时,就会调用该方法。这里我们也可以看到,iface.data不是直接指向接口背后的实际值,而是指向其拷贝,因为这个原因,也就很好理解为什么方法接收者是指针的话,值类型就不会实现对应的接口类型了,因为访问的根本不是同一个变量。

而当把一个指针值赋给一个接口类型的变量呢?编译器会直接生成代码,这个时候iface.data就是该指针值,我们可以写个小demo验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
"fmt"
"unsafe"
)

type eface struct {
typ uintptr
data unsafe.Pointer
}

func main() {
n := struct {
T int
}{}

pi := interface{}(&n)
vi := interface{}(n)

_pi := *(*eface)(unsafe.Pointer(&pi))
_vi := *(*eface)(unsafe.Pointer(&vi))

fmt.Printf("%p %p %p\n", &n, _pi.data, _vi.data) // 0xc000060090 0xc000060090 0x597500
}

代码中我们使用空接口,实际上空接口对应的定义efaceiface的差别只有第一个字段,因为它没有方法表,直接存储的就是值的类型。我们可以看到,接口值vi实际存储的是变量n的地址,而vi.data此时存的就是n的地址。
看一下对应的汇编片段:

1
2
3
4
5
6
7
8
LEAQ	type.struct { T int }(SB), AX
MOVQ AX, (SP)
CALL runtime.newobject(SB) // 代码里面用的unsafe.Pointer,因为传到fmt.Printf里面,逃逸分析认为该变量逃逸了
MOVQ 8(SP), AX // newobject返回的地址
MOVQ AX, ""..autotmp_41+80(SP) // 保存到栈上的临时变量中
LEAQ type.*struct { T int }(SB), CX // *struct{T int}的 _type
MOVQ CX, "".pi+88(SP) // 设置pi的 _type
MOVQ AX, "".pi+96(SP) // 设置pi的data,这里data就是n的地址

实际上在_type.kind中,会有个flag用于记录,该接口的data这个指针,是直接指向实际数据,还是指向一个拷贝:

1
2
3
4
5
// Direct interface types are ptr, chan, map, func, and single-element structs/arrays thereof.
func isDirectIface(t *_type) bool {
// kindDirectIface = 1 << 5
return t.kind&kindDirectIface != 0
}

isisDirectIface返回true,表示直接指向数据。

接口类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 接口转换
func convI2I(inter *interfacetype, i iface) (r iface) {
tab := i.tab
if tab == nil {
return
}
if tab.inter == inter {
r.tab = tab
r.data = i.data
return
}
r.tab = getitab(inter, tab._type, false)
r.data = i.data
return
}

将接口值A强制转换成接口B时,需要满足:接口A的方法集包含或者等于接口B的方法集

接口值之间的类型转换,不会考虑实际类型的方法集,而是简单的对接口的方法集进行判断,这个在编译时就可以进行检查

1
2
3
4
5
6
7
var r io.ReadCloser = XXX{}
r.Read(nil)
_ = io.Reader(r) // ok
_ = io.ReadCloser(r) // ok
_ = io.Writer(r) // no
var rc io.Reader = XXX{}
_ = io.ReaderCloser(rc) // no

接口类型断言

接口断言:根据接口值的实际类型,判断是否实现了目标接口

类型断言时,可能需要在运行时动态生成itab

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// 接口断言,断言失败直接panic
func assertI2I(inter *interfacetype, i iface) (r iface) {
tab := i.tab
// 如果tab为nil,说明接口是nil,直接panic
if tab == nil {
// explicit conversions require non-nil interface value.
panic(&TypeAssertionError{nil, nil, &inter.typ, ""})
}

// 如果目标接口类型就是当前接口类型,直接返回
if tab.inter == inter {
r.tab = tab
r.data = i.data
return
}
r.tab = getitab(inter, tab._type, false) // 获取itab,如果失败直接panic
r.data = i.data
return
}

// 接口断言,第二个返回值表示是否断言成功
func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) {
tab := i.tab
// 接口是nil,返回断言失败
if tab == nil {
return
}
if tab.inter != inter {
tab = getitab(inter, tab._type, true) // true表示容忍失败
if tab == nil { // 不符合,返回false
return
}
}
r.tab = tab
r.data = i.data
b = true
return
}

func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
if len(inter.mhdr) == 0 {
throw("internal error - misuse of itab")
}

// easy case
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

// First, look in the existing table to see if we can find the itab we need.
// This is by far the most common case, so do it without locks.
// Use atomic to ensure we see any previous writes done by the thread
// that updates the itabTable field (with atomic.Storep in itabAdd).
// 先查表是否已经存在需要的itab
t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
if m = t.find(inter, typ); m != nil {
goto finish
}

// Not found. Grab the lock and try again.
lock(&itabLock)
// 双重锁检查
if m = itabTable.find(inter, typ); m != nil {
unlock(&itabLock)
goto finish
}

// Entry doesn't exist yet. Make a new entry & add it.
// 分配itab内存,itab的内存分配在gc堆之外,不会被垃圾扫描、回收
m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
m.inter = inter
m._type = typ
m.init() // 初始化
itabAdd(m) // 添加到itabTable中,后续直接查表,不需要重新构造
unlock(&itabLock)
finish:
if m.fun[0] != 0 { // itab初始化成功
return m
}
if canfail {
return nil
}
// this can only happen if the conversion
// was already done once using the , ok form
// and we have a cached negative result.
// The cached result doesn't record which
// interface function was missing, so initialize
// the itab again to get the missing function name.
panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}


// init fills in the m.fun array with all the code pointers for
// the m.inter/m._type pair. If the type does not implement the interface,
// it sets m.fun[0] to 0 and returns the name of an interface function that is missing.
// It is ok to call this multiple times on the same m, even concurrently.
func (m *itab) init() string {
inter := m.inter
typ := m._type
x := typ.uncommon()

// both inter and typ have method sorted by name,
// and interface names are unique,
// so can iterate over both in lock step;
// the loop is O(ni+nt) not O(ni*nt).
// 接口和类型的方法列表是按照名字排序的,因此实际循环时间复杂度是O(ni+nt)
ni := len(inter.mhdr) // 目标接口方法总数
nt := int(x.mcount) // 实际类型方法总数
// 计算实际类型的方法引用列表的偏移
xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
j := 0
imethods:
for k := 0; k < ni; k++ {
i := &inter.mhdr[k]
itype := inter.typ.typeOff(i.ityp) // 目标接口方法类型,与参数和返回值相关
name := inter.typ.nameOff(i.name) // 目标接口方法名
iname := name.name()
ipkg := name.pkgPath() // 接口的包名
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) //实际函数入口PC
// 保存到itab的方法列表中
*(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn
}
continue imethods
}
}
}
// didn't find method
m.fun[0] = 0 // 没有找到方法,即目标类型没有实现该方法
return iname
}
m.hash = typ.hash
return ""
}

其他

因为go中的接口是隐式实现的,我们可以在声明类型的时候,使用一些断言来加入编译时的检查,并且也可以提示其他人该类型实现了某个接口,比如我们声明了类型T实现了接口I,我们可以:

1
2
3
4
type I interface{}

var _ I = new(T) // 这里断言T实现了接口I
type T struct{}

参考

https://github.com/teh-cmc/go-internals/tree/master/chapter2_interfaces