接口转换的原理
通过前面提到的 iface 的源码可以看到,实际上它包含接口的类型 interfacetype 和 实体类型的类型 _type,这两者都是 iface 的字段 itab 的成员。也就是说生成一个 itab 同时需要接口的类型和实体的类型。
->itable
当判定一种类型是否满足某个接口时,Go 使用类型的方法集和接口所需要的方法集进行匹配,如果类型的方法集完全包含接口的方法集,则可认为该类型实现了该接口。
例如某类型有 m 个方法,某接口有 n 个方法,则很容易知道这种判定的时间复杂度为 O(mn),Go 会对方法集的函数按照函数名的字典序进行排序,所以实际的时间复杂度为 O(m+n)
这里我们来探索将一个接口转换给另外一个接口背后的原理,当然,能转换的原因必然是类型兼容。
直接来看一个例子:
1
package main
2
3
import "fmt"
4
5
type coder interface {
6
code()
7
run()
8
}
9
10
type runner interface {
11
run()
12
}
13
14
type Gopher struct {
15
language string
16
}
17
18
func (g Gopher) code() {
19
return
20
}
21
22
func (g Gopher) run() {
23
return
24
}
25
26
func main() {
27
var c coder = Gopher{}
28
29
var r runner
30
r = c
31
fmt.Println(c, r)
32
}
Copied!
简单解释下上述代码:定义了两个 interface: coderrunner。定义了一个实体类型 Gopher,类型 Gopher 实现了两个方法,分别是 run()code()。main 函数里定义了一个接口变量 c,绑定了一个 Gopher 对象,之后将 c 赋值给另外一个接口变量 r 。赋值成功的原因是 c 中包含 run() 方法。这样,两个接口变量完成了转换。
执行命令:
1
go tool compile -S ./src/main.go
Copied!
得到 main 函数的汇编命令,可以看到: r = c 这一行语句实际上是调用了 runtime.convI2I(SB),也就是 convI2I 函数,从函数名来看,就是将一个 interface 转换成另外一个 interface,看下它的源代码:
1
func convI2I(inter *interfacetype, i iface) (r iface) {
2
tab := i.tab
3
if tab == nil {
4
return
5
}
6
if tab.inter == inter {
7
r.tab = tab
8
r.data = i.data
9
return
10
}
11
r.tab = getitab(inter, tab._type, false)
12
r.data = i.data
13
return
14
}
Copied!
代码比较简单,函数参数 inter 表示接口类型,i 表示绑定了实体类型的接口,r 则表示接口转换了之后的新的 iface。通过前面的分析,我们又知道, iface 是由 tabdata 两个字段组成。所以,实际上 convI2I 函数真正要做的事,找到新 interfacetabdata,就大功告成了。
我们还知道,tab 是由接口类型 interfacetype 和 实体类型 _type。所以最关键的语句是 r.tab = getitab(inter, tab._type, false)
因此,重点来看下 getitab 函数的源码,只看关键的地方:
1
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
2
// ……
3
4
// 根据 inter, typ 计算出 hash 值
5
h := itabhash(inter, typ)
6
7
// look twice - once without lock, once with.
8
// common case will be no lock contention.
9
var m *itab
10
var locked int
11
for locked = 0; locked < 2; locked++ {
12
if locked != 0 {
13
lock(&ifaceLock)
14
}
15
16
// 遍历哈希表的一个 slot
17
for m = (*itab)(atomic.Loadp(unsafe.Pointer(&hash[h]))); m != nil; m = m.link {
18
19
// 如果在 hash 表中已经找到了 itab(inter 和 typ 指针都相同)
20
if m.inter == inter && m._type == typ {
21
// ……
22
23
if locked != 0 {
24
unlock(&ifaceLock)
25
}
26
return m
27
}
28
}
29
}
30
31
// 在 hash 表中没有找到 itab,那么新生成一个 itab
32
m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
33
m.inter = inter
34
m._type = typ
35
36
// 添加到全局的 hash 表中
37
additab(m, true, canfail)
38
unlock(&ifaceLock)
39
if m.bad {
40
return nil
41
}
42
return m
43
}
Copied!
简单总结一下:getitab 函数会根据 interfacetype_type 去全局的 itab 哈希表中查找,如果能找到,则直接返回;否则,会根据给定的 interfacetype_type 新生成一个 itab,并插入到 itab 哈希表,这样下一次就可以直接拿到 itab
这里查找了两次,并且第二次上锁了,这是因为如果第一次没找到,在第二次仍然没有找到相应的 itab 的情况下,需要新生成一个,并且写入哈希表,因此需要加锁。这样,其他协程在查找相同的 itab 并且也没有找到时,第二次查找时,会被挂住,之后,就会查到第一个协程写入哈希表的 itab
再来看一下 additab 函数的代码:
1
// 检查 _type 是否符合 interface_type 并且创建对应的 itab 结构体 将其放到 hash 表中
2
func additab(m *itab, locked, canfail bool) {
3
inter := m.inter
4
typ := m._type
5
x := typ.uncommon()
6
7
// both inter and typ have method sorted by name,
8
// and interface names are unique,
9
// so can iterate over both in lock step;
10
// the loop is O(ni+nt) not O(ni*nt).
11
//
12
// inter 和 typ 的方法都按方法名称进行了排序
13
// 并且方法名都是唯一的。所以循环的次数是固定的
14
// 只用循环 O(ni+nt),而非 O(ni*nt)
15
ni := len(inter.mhdr)
16
nt := int(x.mcount)
17
xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
18
j := 0
19
for k := 0; k < ni; k++ {
20
i := &inter.mhdr[k]
21
itype := inter.typ.typeOff(i.ityp)
22
name := inter.typ.nameOff(i.name)
23
iname := name.name()
24
ipkg := name.pkgPath()
25
if ipkg == "" {
26
ipkg = inter.pkgpath.name()
27
}
28
for ; j < nt; j++ {
29
t := &xmhdr[j]
30
tname := typ.nameOff(t.name)
31
// 检查方法名字是否一致
32
if typ.typeOff(t.mtyp) == itype && tname.name() == iname {
33
pkgPath := tname.pkgPath()
34
if pkgPath == "" {
35
pkgPath = typ.nameOff(x.pkgpath).name()
36
}
37
if tname.isExported() || pkgPath == ipkg {
38
if m != nil {
39
// 获取函数地址,并加入到itab.fun数组中
40
ifn := typ.textOff(t.ifn)
41
*(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn
42
}
43
goto nextimethod
44
}
45
}
46
}
47
// ……
48
49
m.bad = true
50
break
51
nextimethod:
52
}
53
if !locked {
54
throw("invalid itab locking")
55
}
56
57
// 计算 hash 值
58
h := itabhash(inter, typ)
59
// 加到Hash Slot链表中
60
m.link = hash[h]
61
m.inhash = true
62
atomicstorep(unsafe.Pointer(&hash[h]), unsafe.Pointer(m))
63
}
Copied!
additab 会检查 itab 持有的 interfacetype_type 是否符合,就是看 _type 是否完全实现了 interfacetype 的方法,也就是看两者的方法列表重叠的部分就是 interfacetype 所持有的方法列表。注意到其中有一个双层循环,乍一看,循环次数是 ni * nt,但由于两者的函数列表都按照函数名称进行了排序,因此最终只执行了 ni + nt 次,代码里通过一个小技巧来实现:第二层循环并没有从 0 开始计数,而是从上一次遍历到的位置开始。
求 hash 值的函数比较简单:
1
func itabhash(inter *interfacetype, typ *_type) uint32 {
2
h := inter.typ.hash
3
h += 17 * typ.hash
4
return h % hashSize
5
}
Copied!
hashSize 的值是 1009。
更一般的,当把实体类型赋值给接口的时候,会调用 conv 系列函数,例如空接口调用 convT2E 系列、非空接口调用 convT2I 系列。这些函数比较相似:
  1. 1.
    具体类型转空接口时,_type 字段直接复制源类型的 _type;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。
  2. 2.
    具体类型转非空接口时,入参 tab 是编译器在编译阶段预先生成好的,新接口 tab 字段直接指向入参 tab 指向的 itab;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。
  3. 3.
    而对于接口转接口,itab 调用 getitab 函数获取。只用生成一次,之后直接从 hash 表中获取。

参考资料

【和 C++ 的对比】https://www.jianshu.com/p/b38b1719636e
Copy link
Contents
参考资料