1. 接口的基本使用
golang 中的 interface 本身也是一种类型, 它代表的是一个方法的集合. 任何类型只要实现了接口中声明的所有方法, 那么该类就实现了该接口. 与其他语言不同, golang 并不需要显式声明类型实现了某个接口, 而是由编译器和 runtime 进行检查.
声明
type 接口名 interface{
方法 1
方法 2
...
方法 n
}
type 接口名 interface {
已声明接口名 1
...
已声明接口名 n
- }
- type iface interface{
- tab *itab
- data unsafe.Pointer
- }
接口自身也是一种结构类型, 只是编译器对其做了很多限制:
不能有字段
不能定义自己的方法
只能声明方法, 不能实现
可嵌入其他接口类型
- package main
- import (
- "fmt"
- )
- // 定义一个接口
- type People interface {
- ReturnName() string
- }
- // 定义一个结构体
- type Student struct {
- Name string
- }
- // 定义结构体的一个方法.
- // 突然发现这个方法同接口 People 的所有方法 (就一个), 此时可直接认为结构体 Student 实现了接口 People
- func (s Student) ReturnName() string {
- return s.Name
- }
- func main() {
- cbs := Student{Name:"小明"}
- var a People
- // 因为 Students 实现了接口所以直接赋值没问题
- // 如果没实现会报错: cannot use cbs (type Student) as type People in assignment:Student does not implement People (missing ReturnName method)
- a = cbs
- name := a.ReturnName()
- fmt.Println(name) // 输出 "小明"
- }
如果一个接口不包含任何方法, 那么它就是一个空接口 (empty interface), 所有类型都符合 empty interface 的定义, 因此任何类型都能转换成 empty interface.
接口的值简单来说, 是由两部分组成的, 就是类型和数据, 判断两个接口是相等, 就是看他们的这两部分是否相等; 另外类型和数据都为 nil 才代表接口是 nil.
- var a interface{}
- var b interface{} = (*int)(nil)
- fmt.Println(a == nil, b == nil) //true false
2. 接口嵌套
像匿名字段那样嵌入其他接口. 目标类型方法集中必须拥有包含嵌入接口方法在内的全部方法才算实现了该接口. 嵌入其他接口类型相当于将其声明的方法集中导入. 这就要求不能有同名方法, 不能嵌入自身或循环嵌入.
- type stringer interfaceP{
- string() string
- }
- type tester interface {
- stringer
- test()
- }
- type data struct{}
- func (*data) test() {}
- func (data) string () string {
- return ""
- }
- func main() {
- var d data
- var t tester = &d
- t.test()
- println(t.string())
- }
超集接口变量可隐式转换为子集, 反过来不行.
3. 接口的实现
golang 的接口检测既有静态部分, 也有动态部分.
静态部分
对于具体类型 (concrete type, 包括自定义类型) -> interface, 编译器生成对应的 itab 放到 ELF 的. rodata 段, 后续要获取 itab 时, 直接把指针指向存在. rodata 的相关偏移地址即可. 具体实现可以看 golang 的提交日志 CL 20901,CL 20902.
对于 interface-> 具体类型 (concrete type, 包括自定义类型), 编译器提取相关字段进行比较, 并生成值
动态部分
在 runtime 中会有一个全局的 hash 表, 记录了相应 type->interface 类型转换的 itab, 进行转换时候, 先到 hash 表中查, 如果有就返回成功; 如果没有, 就检查这两种类型能否转换, 能就插入到 hash 表中返回成功, 不能就返回失败. 注意这里的 hash 表不是 go 中的 map, 而是一个最原始的使用数组的 hash 表, 使用开放地址法来解决冲突. 主要是 interface <-> interface(接口赋值给接口, 接口转换成另一接口) 使用到动态生产 itab.
interface 的结构如下:
接口类型的结构 interfacetype
- type interfacetype struct {
- typ _type
- pkgpath name // 记录定义接口的包名
- mhdr []imethod // 一个 imethod 切片, 记录接口中定义的那些函数.
- }
- // imethod 表示接口类型上的方法
- type imethod struct {
- name nameOff // name of method
- typ typeOff // .(*FuncType) underneath
- }
nameOff 和 typeOff 类型是 int32 , 这两个值是链接器负责嵌入的, 相对于可执行文件的元信息的偏移量. 元信息会在运行期, 加载到 runtime.moduledata 结构体中.
4. 接口值的结构 iface 和 eface
为了性能, golang 专门分了两种 interface,eface 和 iface,eface 就是空接口, iface 就是有方法的接口.
- type iface struct {
- tab *itab
- data unsafe.Pointer
- }
- type eface struct {
- _type *_type
- data unsafe.Pointer
- }
- type itab struct {
- inter *interfacetype //inter 接口类型
- _type *_type //_type 数据类型
- hash uint32 //_type.hash 的副本. 用于类型开关. hash 哈希的方法
- _ [4]byte
- fun [1]uintptr // 大小可变. fun [0] == 0 表示_type 未实现 inter. fun 函数地址占位符
- }
iface 结构体中的 data 是用来存储实际数据的, runtime 会申请一块新的内存, 把数据考到那, 然后 data 指向这块新的内存.
itab 中的 hash 方法拷贝自_type.hash;fun 是一个大小为 1 的 uintptr 数组, 当 fun[0] 为 0 时, 说明_type 并没有实现该接口, 当有实现接口时, fun 存放了第一个接口方法的地址, 其他方法一次往下存放, 这里就简单用空间换时间, 其实方法都在_type 字段中能找到, 实际在这记录下, 每次调用的时候就不用动态查找了.
4.1 全局的 itab table
- iface.go:
- const itabInitSize = 512
- // 注意: 如果更改这些字段, 请在 itabAdd 的 mallocgc 调用中更改公式.
- type itabTableType struct {
- size uintptr // 条目数组的长度. 始终为 2 的幂.
- count uintptr // 当前已填写的条目数.
- entries [itabInitSize]*itab // really [size] large
- }
可以看出这个全局的 itabTable 是用数组在存储的, size 记录数组的大小, 总是 2 的次幂. count 记录数组中已使用了多少. entries 是一个 * itab 数组, 初始大小是 512.
5. 接口类型转换
把一个具体的值, 赋值给接口, 会调用 conv 系列函数, 例如空接口调用 convT2E 系列, 非空接口调用 convT2I 系列, 为了性能考虑, 很多特例的 convT2I64,convT2Estring 诸如此类, 避免了 typedmemmove 的调用.
- func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
- if raceenabled {
- raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
- }
- if msanenabled {
- msanread(elem, t.size)
- }
- x := mallocgc(t.size, t, true)
- // TODO: 我们分配一个清零的对象只是为了用实际数据覆盖它.
- // 确定如何避免归零. 同样在下面的 convT2Eslice,convT2I,convT2Islice 中.
- typedmemmove(t, x, elem)
- e._type = t
- e.data = x
- return
- }
- 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)
- typedmemmove(t, x, elem)
- i.tab = tab
- i.data = x
- return
- }
- func convT2I16(tab *itab, val uint16) (i iface) {
- t := tab._type
- var x unsafe.Pointer
- if val == 0 {
- x = unsafe.Pointer(&zeroVal[0])
- } else {
- x = mallocgc(2, t, false)
- *(*uint16)(x) = val
- }
- i.tab = tab
- i.data = x
- return
- }
- 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
- }
可以看出:
具体类型转空接口,_type 字段直接复制源的 type;mallocgc 一个新内存, 把值复制过去, data 再指向这块内存.
具体类型转非空接口, 入参 tab 是编译器生成的填进去的, 接口指向同一个入参 tab 指向的 itab;mallocgc 一个新内存, 把值复制过去, data 再指向这块内存.
对于接口转接口, itab 是调用 getitab 函数去获取的, 而不是编译器传入的.
对于那些特定类型的值, 如果是零值, 那么不会 mallocgc 一块新内存, data 会指向 zeroVal[0].
5.1 接口转接口
- func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) {
- tab := i.tab
- if tab == nil {
- return
- }
- if tab.inter != inter {
- tab = getitab(inter, tab._type, true)
- if tab == nil {
- return
- }
- }
- r.tab = tab
- r.data = i.data
- b = true
- return
- }
- func assertE2I(inter *interfacetype, e eface) (r iface) {
- t := e._type
- if t == nil {
- // 显式转换需要非 nil 接口值.
- panic(&TypeAssertionError{nil, nil, &inter.typ, ""})
- }
- r.tab = getitab(inter, t, false)
- r.data = e.data
- return
- }
- func assertE2I2(inter *interfacetype, e eface) (r iface, b bool) {
- t := e._type
- if t == nil {
- return
- }
- tab := getitab(inter, t, true)
- if tab == nil {
- return
- }
- r.tab = tab
- r.data = e.data
- b = true
- return
- }
我们看到有两种用法:
返回值是一个时, 不能转换就 panic.
返回值是两个时, 第二个返回值标记能否转换成功
此外, data 复制的是指针, 不会完整拷贝值. 每次都 malloc 一块内存, 那么性能会很差, 因此, 对于一些类型, golang 的编译器做了优化.
5.2 接口转具体类型
接口判断是否转换成具体类型, 是编译器生成好的代码去做的. 我们看个 empty interface 转换成具体类型的例子:
- var EFace interface{}
- var j int
- func F4(i int) int{
- EFace = I
- j = EFace.(int)
- return j
- }
- func main() {
- F4(10)
- }
反汇编:
- go build -gcflags '-N -l' -o tmp build.go
- go tool objdump -s "main.F4" tmp
可以看汇编代码:
- MOVQ main.EFace(SB), CX //CX = EFace.typ
- LEAQ type.*+60128(SB), DX //DX = &type.int
- CMPQ DX, CX. //if DX == AX
可以看到 empty interface 转具体类型, 是编译器生成好对比代码, 比较具体类型和空接口是不是同一个 type, 而不是调用某个函数在运行时动态对比.
5.3 非空接口类型转换
- var tf Tester
- var t testStruct
- func F4() int{
- t := tf.(testStruct)
- return t.i
- }
- func main() {
- F4()
- }
- // 反汇编
- MOVQ main.tf(SB), CX // CX = tf.tab(.inter.typ)
- LEAQ go.itab.main.testStruct,main.Tester(SB), DX // DX = <testStruct,Tester > 对应的 & itab(.inter.typ)
- CMPQ DX, CX //
可以看到, 非空接口转具体类型, 也是编译器生成的代码, 比较是不是同一个 itab, 而不是调用某个函数在运行时动态对比.
6. 获取 itab 的流程
golang interface 的核心逻辑就在这, 在 get 的时候, 不仅仅会从 itabTalbe 中查找, 还可能会创建插入, itabTable 使用容量超过 75% 还会扩容. 看下代码:
- func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
- if len(inter.mhdr) == 0 {
- throw("internal error - misuse of itab")
- }
- // 简单的情况
- 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
- // 首先, 查看现有表以查看是否可以找到所需的 itab.
- // 这是迄今为止最常见的情况, 因此请不要使用锁.
- // 使用 atomic 确保我们看到该线程完成的所有先前写入更新 itabTable 字段 (在 itabAdd 中使用 atomic.Storep).
- t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
- if m = t.find(inter, typ); m != nil {
- goto finish
- }
- // 未找到. 抓住锁, 然后重试.
- lock(&itabLock)
- if m = itabTable.find(inter, typ); m != nil {
- unlock(&itabLock)
- goto finish
- }
- // 条目尚不存在. 进行新输入并添加.
- 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)
- unlock(&itabLock)
- finish:
- if m.fun[0] != 0 {
- return m
- }
- if canfail {
- return nil
- }
- // 仅当转换时才会发生, 使用 ok 形式已经完成一次, 我们得到了一个缓存的否定结果.
- // 缓存的结果不会记录, 缺少接口函数, 因此初始化再次获取 itab, 以获取缺少的函数名称.
- panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
- }
流程如下:
先用 t 保存全局 itabTable 的地址, 然后使用 t.find 去查找, 这样是为了防止查找过程中, itabTable 被替换导致查找错误.
如果没找到, 那么就会上锁, 然后使用 itabTable.find 去查找, 这样是因为在第一步查找的同时, 另外一个协程写入, 可能导致实际存在却查找不到, 这时上锁避免 itabTable 被替换, 然后直接在 itaTable 中查找.
再没找到, 说明确实没有, 那么就根据接口类型, 数据类型, 去生成一个新的 itab, 然后插入到 itabTable 中, 这里可能会导致 hash 表扩容, 如果数据类型并没有实现接口, 那么根据调用方式, 该报错报错, 该 panic panic.
这里我们可以看到申请新的 itab 空间时, 内存空间的大小是 unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 参照前面接受的结构, len(inter.mhdr) 就是接口定义的方法数量, 因为字段 fun 是一个大小为 1 的数组, 所以 len(inter.mhdr)-1, 在 fun 字段下面其实隐藏了其他方法接口地址.
6.1 在 itabTable 中查找 itab find
- func itabHashFunc(inter *interfacetype, typ *_type) uintptr {
- // 编译器为我们提供了一些很好的哈希码.
- return uintptr(inter.typ.hash ^ typ.hash)
- }
- // find 在 t 中找到给定的接口 / 类型对.
- // 如果不存在给定的接口 / 类型对, 则返回 nil.
- func (t *itabTableType) find(inter *interfacetype, typ *_type) *itab {
- // 使用二次探测实现.
- // 探测顺序为 h(i)= h0 + i *(i + 1)/ 2 mod 2 ^ k.
- // 我们保证使用此探测序列击中所有表条目.
- mask := t.size - 1
- h := itabHashFunc(inter, typ) & mask
- for i := uintptr(1); ; i++ {
- p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize))
- // 在这里使用 atomic read, 所以如果我们看到 m!= nil, 我们也会看到 m 字段的初始化.
- // m := *p
- 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
- }
- }
从注释可以看到, golang 使用的开放地址探测法, 用的是公式 h(i) = h0 + i*(i+1)/2 mod 2^k,h0 是根据接口类型和数据类型的 hash 字段算出来的. 以前的版本是额外使用一个 link 字段去连到下一个 slot, 那样会有额外的存储, 性能也会差写, 在 1.11 中我们看到有了改进.
6.2 检查并生成 itab init
- // init 用所有代码指针填充 m.fun 数组 m.inter / m._type 对. 如果该类型未实现该接口, 将 m.fun [0] 设置为 0, 并返回缺少的接口函数的名称.
- // 可以在同一 m 上多次调用此函数, 即使同时调用也可以.
- func (m *itab) init() string {
- inter := m.inter
- typ := m._type
- x := typ.uncommon()
- // inter 和 typ 都有按名称排序的方法,
- // 并且接口名称是唯一的,
- // 因此可以在锁定步骤中对两者进行迭代;
- // 循环是 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)
- *(*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 ""
- }
这个方法会检查 interface 和 type 的方法是否匹配, 即 type 有没有实现 interface. 假如 interface 有 n 中方法, type 有 m 中方法, 那么匹配的时间复杂度是 O(n x m), 由于 interface,type 的方法都按字典序排, 所以 O(n+m) 的时间复杂度可以匹配完. 在检测的过程中, 匹配上了, 依次往 fun 字段写入 type 中对应方法的地址. 如果有一个方法没有匹配上, 那么就设置 fun[0] 为 0, 在外层调用会检查 fun[0]==0, 即 type 并没有实现 interface.
这里我们还可以看到 golang 中 continue 的特殊用法, 要直接 continue 到外层的循环中, 那么就在那一层的循环上加个标签, 然后 continue 标签.
6.3 把 itab 插入到 itabTable 中 itabAdd
- // itabAdd 将给定的 itab 添加到 itab 哈希表中.
- // 必须保持 itabLock.
- func itabAdd(m *itab) {
- // 设置了 mallocing 时, 错误可能导致调用此方法, 通常是因为这是在恐慌时调用的.
- // 可靠地崩溃, 而不是仅在需要增长时崩溃哈希表.
- if getg().m.mallocing != 0 {
- throw("malloc deadlock")
- }
- t := itabTable
- if t.count>= 3*(t.size/4) { // 75% 负载系数
- // 增长哈希表.
- // t2 = new(itabTableType)+ 一些其他条目我们撒谎并告诉 malloc 我们想要无指针的内存, 因为所有指向的值都不在堆中.
- t2 := (*itabTableType)(mallocgc((2+2*t.size)*sys.PtrSize, nil, true))
- t2.size = t.size * 2
- // 复制条目.
- // 注意: 在复制时, 其他线程可能会寻找 itab 和找不到它. 没关系, 他们将尝试获取 Itab 锁, 因此请等到复制完成.
- if t2.count != t.count {
- throw("mismatched count during itab table copy")
- }
- // 发布新的哈希表. 使用原子写入: 请参阅 getitab 中的注释.
- atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2))
- // 采用新表作为我们自己的表.
- t = itabTable
- // 注意: 旧表可以在此处进行 GC 处理.
- }
- t.add(m)
- }
- // add 将给定的 itab 添加到 itab 表 t 中.
- // 必须保持 itabLock.
- func (t *itabTableType) add(m *itab) {
- // 请参阅注释中的有关探查序列的注释.
- // 将新的 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 {
- // 给定的 itab 可以在多个模块中使用并且由于全局符号解析的工作方式,
- // 指向 itab 的代码可能已经插入了全局 "哈希".
- return
- }
- if m2 == nil {
- // 在这里使用原子写, 所以如果读者看到 m, 它也会看到正确初始化的 m 字段.
- // NoWB 正常, 因为 m 不在堆内存中.
- //*p = m
- atomic.StorepNoWB(unsafe.Pointer(p), unsafe.Pointer(m))
- t.count++
- return
- }
- h += I
- h &= mask
- }
- }
可以看到, 当 hash 表使用达到 75% 或以上时, 就会进行扩容, 容量是原来的 2 倍, 申请完空间, 就会把老表中的数据插入到新的 hash 表中. 然后使 itabTable 指向新的表, 最后把新的 itab 插入到新表中.
来源: https://www.cnblogs.com/33debug/p/11867306.html