Ezreal 书架 Ezreal 书架
Home
  • 《Go程序员面试笔试宝典》
  • 《RabbitMQ 实战指南》
  • 《深入理解kafka》
  • MySQL45讲
  • 透视HTTP协议
  • 结构化数据的分布式存储系统
  • Raft 共识算法
Home
  • 《Go程序员面试笔试宝典》
  • 《RabbitMQ 实战指南》
  • 《深入理解kafka》
  • MySQL45讲
  • 透视HTTP协议
  • 结构化数据的分布式存储系统
  • Raft 共识算法
  • 逃逸分析

  • 延迟语句

  • 数据容器

  • 通道

  • 接口

    • Go 接口与 C++接口有何异同
    • Go 语言与“鸭子类型”的关系
    • iface 和 eface 的区别是什么
    • 值接收者和指针接收者的区别
    • 如何用 interface 实现多态
    • 接口的动态类型和动态值是什么
    • 接口转换的原理是什么
    • 类型转换和断言的区别是什么
    • 如何让编译器自动检测类型是否实现了接口
  • unsafe

  • context

  • Go程序员面试笔试宝典
  • 接口
ezreal_rao
2023-06-01

接口转换的原理是什么

通过前面讲到的 iface 源码可以看到,iface 包含接口的类型 interfacetype 和实体类型的类型 _type,两者都是 iface 的字段 itab 的成员。也就是说生成一个 itab 同时需要接口的类型和实体的类型。

<interface 类型,实体类型> -> itab
1

当判定一种类型是否满足某个接口时,Go 将类型的方法集和接口所需要的方法集进行匹配, 如果类型的方法集完全包含接口的方法集,则可认为该类型实现了该接口。 例如, 某类型有 m 个方法, 某接口有 n 个方法, 很容易知道这种判定的时间复杂度为 O (mn),Go 会对方法集的函数按照函数名的字典序进行排序,所以实际的时间复杂度为 O (m+n)。

本节来探索将一个接口转换为另外一个接口背后的原理,当然,能转换的原因必然是类型兼容。

来看一个例子:

package main

import "fmt"

type coder interface {
	code()
	run()
}

type runner interface {
	run()
}

type Gopher struct {
	language string
}

func (g Gopher) code() {
	return
}

func (g Gopher) run() {}

func main() {
	var c coder = Gopher{}
	var r runner

	r = c
	fmt.Println(c, r)
}
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

简单解释下上述代码,定义了两个 interface: coder 和 runner,定义了一个实体类型 Gopher, 类型 Gopher 实现了两个方法,分别是 run () 和 code ()。在 main 函数里定义了一个接口变量 c, 绑定了一个 Gopher 对象,之后将 c 赋值给另外一个接口变量 r 。赋值成功的原因是 c 中包含 run () 方法。这样,两个接口变量完成了转换。

执行命令:

go tool compile -S main.go
1

得到 main 函数的汇编命令,可以看到:r = c 这一行语句实际上是调用了 runtime.convI2I (SB),也就是 convI2I 函数,从函数名来看,就是将一个 interface 转换成另外一个 interface,来看它的源代码:

// src/runtime/iface.go

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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

代码比较简单,函数参数 inter 表示接口类型,i 表示绑定了实体类型的接口,r 则表示完成了接口转换了之后的新的 iface。通过前面的分析得知,iface 是由 tab 和 data 两个字段组成。所以,实际上 convI2I 函数真正要做的事,找到新 interface 的 tab 和 data,就大功告成了。

并且, tab 由接口类型 interfacetype 和实体类型 _type 组成, 所以最关键的语句是 r.tab = getitab (inter, tab._type, false)。

因此,重点来看下 getitab 函数的源码,看下关键的地方:

// src/runtime/iface.go

func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
    // ……
    var m *itab
    t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
    if m = t.find(inter, typ); m != nil { // 根据 inter, typ 在 itabTable 中寻找 itab
        goto finish
    }
    // 没找到,上锁
    lock(&itabLock)
    if m = itabTable.find(inter, typ); m != nil { // 再找一次
    unlock(&itabLock)
        goto finish
    }
    // 在哈希表中没有找到 itab,那就新生成一个 itab
    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) // 添加到全局的 itab 表中
    unlock(&itabLock)
finish:
    if m.fun[0] != 0 {
        return m
    }
    if canfail {
        return nil
    }
    panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}
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

首先,getitab 函数会根据 interfacetype 和 _type 去全局的 itab 哈希表中查找,如果能找到, 则直接返回;否则,会根据给定的 interfacetype 和 _type 新生成一个 itab,并插入到 itab 哈希表,这样下一次就可以直接拿到 itab。

在 getitab 函数中,进行了两次查找,并且第二次加上了锁,这是因为如果第一次没找到,在第二次仍然没有找到相应的 itab 的情况下,需要新生成一个,并且写入到 itab 哈希表,加锁保证并发安全。

来看下 find 函数的代码:

// src/runtime/iface.go

func (t *itabTableType) find(inter *interfacetype, typ *_type) *itab {
    mask := t.size - 1
    h := itabHashFunc(inter, typ) & mask
    for i := uintptr(1); ; i++ {
        p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize))
        m := (*itab)(atomic.Loadp(unsafe.Pointer(p)))
        if m == nil {
            return nil
        }
        if m.inter == inter && m._type == typ {
            return m
        }
        h += i
        h &= mask
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

参数类型 itabTableType 的定义如下,核心就是 entries,但实际上是一个 itab 指针数组:

// src/runtime/iface.go

type itabTableType struct {
    size uintptr
    count uintptr
    entries [itabInitSize]*itab
}
1
2
3
4
5
6
7

find 函数会调用 itabHashFunc 求出哈希值,根据哈希值定位到 itab 在 entries 中的位置,找出已缓存的 itab。如果没有找到,则返回 nil。

回到 getitab 函数,若没有找到缓存的 itab,则新建一个 itab,并通过 itabAdd 函数将其加入 entries 中。而 itabAdd 的核心就是 add 函数:

// src/runtime/iface.go

func (t *itabTableType) add(m *itab) {
    mask := t.size - 1
    h := itabHashFunc(m.inter, m._type) & mask
    for i := uintptr(1); ; i++ {
        p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize))
        m2 := *p
        if m2 == m {
            return
        }
        if m2 == nil {
            atomic.StorepNoWB(unsafe.Pointer(p), unsafe.Pointer(m))
            t.count++
            return
        }
        h += i
        h &= mask
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

可以看到,add 函数的逻辑和 find 函数相似,都是先求出 hash 值,然后定位到 entries 中的 位置,再将 itab 保存到 entries 中。求 hash 值的函数如下:

// src/runtime/iface.go

func itabHashFunc(inter *interfacetype, typ *_type) uintptr {
    // compiler has provided some good hash codes for us.
    return uintptr(inter.typ.hash ^ typ.hash)
}
1
2
3
4
5
6

更一般地, 当把实体类型赋值给接口的时候, 会调用 conv 系列函数, 例如空接口调用 convT2E 系列、非空接口调用 convT2I 系列。这些函数比较相似:

1)具体类型转空接口时,_type 字段直接复制源类型的 _type;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。

2)具体类型转非空接口时,入参 tab 是编译器在编译阶段预先生成好的,新接口 tab 字段直接指向入参 tab 指向的 itab;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。

3)而对于接口转接口,itab 调用 getitab 函数获取,只用生成一次,之后直接从 itab 哈希表中获取。

#golang#interface
上次更新: 7/12/2024, 2:37:05 PM
接口的动态类型和动态值是什么
类型转换和断言的区别是什么

← 接口的动态类型和动态值是什么 类型转换和断言的区别是什么→

最近更新
01
为什么我的MySQL会抖一下
07-15
02
HTTP 性能优化面面观
07-12
03
WebSocket:沙盒里的 TCP
07-12
更多文章>
Theme by Vdoing | Copyright © 2022-2024 Ezreal Rao | CC BY-NC-SA 4.0
豫ICP备2023001810号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式