六. go 常见数据结构实现原理之 interface

一. interface 底层结构

  1. 深入解析Go
  2. Go 语言问题集(Go Questions)
  3. interface是go多态的落地,空的interface可以被当作“鸭子”类型使用,它使得Go这样的静态语言拥有了一定的动态性,但却又不损失静态语言在类型安全方面拥有的编译时检查的优势
  4. interface根据内部是否绑定方法,在底层实现上分两种结构
  1. 空接口: Eface
  2. 带方法的接口: Iface

空接口的 Eface

  1. Eface是空接口的底层实现, 内部包含一个void*指针属性,和一个类型结构体的指针type属性,通过类型信息,可以实现反射
struct Eface{
    Type*    type;
    void*    data;
};
  1. 内部type结构详解, Type是类型信息结构体中公共的部分,其中size描述类型的大小,hash数据的hash值,align是对齐,fieldAlgin是这个数据嵌入结构体时的对齐,kind是一个枚举值,每种类型对应了一个编号。alg是一个函数指针的数组,存储了hash/equal/print/copy四个函数操作。UncommonType是指向一个函数指针的数组,收集了这个类型的实现的所有方法
  2. reflect包中有个KindOf函数,返回一个interface{}的Type,其实该函数就是简单的取Eface中的Type域
struct Type{
    uintptr size;
    uint32 hash;
    uint8 _unused;
    uint8 align;
    uint8 fieldAlign;
    uint8 kind;
    Alg *alg;
    void *gc;
    String *string;
    UncommonType *x;
    Type *ptrto;
};

带方法的接口 Iface

  1. 带方法的接口底层实现:
struct Iface{
    Itab*    tab;
    void*    data;
};
  1. Iface和Eface略有不同,data域同样是指向原始数据的,不同的是tab类型的指针属性:
struct Itab{
    InterfaceType*    inter;
    Type*    type;
    Itab*    link;
    int32    bad;
    int32    unused;
    void    (*fun[])(void);
};
  1. Itab中不仅存储了Type信息,而且还多了一个方法表fun[]。一个Iface中的具体类型中实现的方法会被拷贝到Itab的fun数组中
  2. 有些博客中有提到ifaceWords ,但是在接口相关的源码没找到,我也不知道是否正确,此处就先记录一下(我看实际是在原子相关操作中有看到ifaceWords)

//1. 在使用接口时,空接口类型底层使用Eface,如下声明类一个变量赋值给interface,这个时候底层使用的是Eface
var x interface{} = 10

//2. 带方法的接口底层使用Iface,如下,声明了一个interface类型的结构体,
//内部存在一个抽象方法,在使用该类型变量时底层使用的就是Iface
type MyInterface interface {
    Run()
}

type MyStruct struct{}

func (s MyStruct) Run() {
    fmt.Println("I am running")
}

//3. 在Go的底层提供了一个ifaceWords结构体,内部含 typ 和 data 两个指针类型属性,
//前者表示值的真实类型,后者表示值的“值”
type ifaceWords struct {
    typ  unsafe.Pointer
    data unsafe.Pointer
}
//4. 像"接口变量转换","接口变量比较","接口赋值"等动作,在Golang底层实际都会用到ifaceWords:
//> 1. 接口变量转换: 比如实现了接口 A 的结构体类型转换为接口 B。转换过程中,需要判断 A 是否满足接口 B 的要求。这个过程中,Golang 底层就会使用到 ifaceWords,通过判断两个接口变量的 typ 指针是否相同来判断它们是否是同一种类型。如果不是同一种类型,就无法进行类型转换。
//> 2. 接口变量比较: 接口变量在比较过程中,Golang 底层也会使用到 ifaceWords,底层会先比较两个接口变量的 typ 指针,如果相同,再比较它们存储的实际值。
//> 3. 接口赋值: 接口类型是一种值类型,可以进行赋值操作。将接口变量赋值给另一个接口变量,底层会使用 ifaceWords ,先判断两个接口变量的 typ 指针是否相同,如果不同,则需要进行类型转换。然后,底层会将源接口变量的实际数据拷贝到目标接口变量的实际数据中,完成接口变量的赋值操作。

//5. 相对于 Eface 和 Iface,ifaceWords 更加底层,在下方接口类型赋值和接口转换原理源码讲解中可能没有直接看到使用ifaceWords ,单实际也都间接的使用到了ifaceWords, ifaceWords 它直接存储了具体类型信息和实际数据指针,因此更加高效。在编写底层代码时,会直接使用 ifaceWords 进行相关操作

二. 接口类型的赋值

  1. 先看一个问题: 会正常运行吗? 编译报错
type I interface {
    String()
}
var a int = 5
var b I = a
  1. 由上面的问题我们可以知道.具体类型转换为带方法的接口类型是在编译过程中进行检测的,怎么检测的?
  1. 查看interface的底层结构,Eface结构时内部存在一个Type类型的属性,在Type属性中存在一个UncommonType字段,该字段中存在一个方法表,
  2. interface底层使用Iface结构时,Itab属性中有个InterfaceType字段,这个字段中也有方法表
  3. 类型所实现的方法都会放到这个方法表中,就是这个接口所要求的方法。在向接口赋值时会遍历比较,还会将Type方法表中的函数指针,拷贝到Itab的fun字段中,如果比较不通过则失败

三. 接口转换原理

  1. 将一个实际类型变量赋值给一个接口类型时,底层原理,如下
func main() {
	//1.实际类型
    var c coder = Gopher{}
    //2.接口类型
    var r runner
    //将实际类型赋值给接口类型
    r = c
    fmt.Println(c, r)
}
  1. 在执行了赋值以后,底层实际会执行convI2I()函数
  1. 函数参数 inter 表示接口类型,i 表示绑定了实体类型的接口,r 则表示接口转换了之后的新的 iface。通过前面的分析 iface 是由 tab 和 data 两个字段组成。所以,实际上 convI2I 函数真正要做的事,找到新 interface 的 tab 和 data
  2. tab 是由接口类型 interfacetype 和 实体类型 _type,内部会执行getitab()
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. 查看getitab():
  1. 该函数会根据 interfacetype 和 _type 去全局的 itab 哈希表中查找,如果能找到,则直接返回;否则,会根据给定的 interfacetype 和 _type 新生成一个 itab,并插入到 itab 哈希表,这样下一次就可以直接拿到 itab
  2. 这里查找了两次,并且第二次上锁了,这是因为如果第一次没找到,在第二次仍然没有找到相应的 itab 的情况下,需要新生成一个,并且写入哈希表,因此需要加锁。这样,其他协程在查找相同的 itab 并且也没有找到时,第二次查找时,会被挂住,之后,就会查到第一个协程写入哈希表的 itab
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
    // ……
    // 根据 inter, typ 计算出 hash 值
    h := itabhash(inter, typ)
    // look twice - once without lock, once with.
    // common case will be no lock contention.
    var m *itab
    var locked int
    for locked = 0; locked < 2; locked++ {
        if locked != 0 {
            lock(&ifaceLock)
        }
        // 遍历哈希表的一个 slot
        for m = (*itab)(atomic.Loadp(unsafe.Pointer(&hash[h]))); m != nil; m = m.link {
            // 如果在 hash 表中已经找到了 itab(inter 和 typ 指针都相同)
            if m.inter == inter && m._type == typ {
                // ……
                if locked != 0 {
                    unlock(&ifaceLock)
                }
                return m
            }
        }
    }
    // 在 hash 表中没有找到 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
    // 添加到全局的 hash 表中
    additab(m, true, canfail)
    unlock(&ifaceLock)
    if m.bad {
        return nil
    }
    return m
}
  1. 查看additab 函数的代码
// 检查 _type 是否符合 interface_type 并且创建对应的 itab 结构体 将其放到 hash 表中
func additab(m *itab, locked, canfail bool) {
    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).
    // 
    // 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
    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 {
                        // 获取函数地址,并加入到itab.fun数组中
                        ifn := typ.textOff(t.ifn)
                        *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn
                    }
                    goto nextimethod
                }
            }
        }
        // ……
        m.bad = true
        break
    nextimethod:
    }
    if !locked {
        throw("invalid itab locking")
    }
    // 计算 hash 值
    h := itabhash(inter, typ)
    // 加到Hash Slot链表中
    m.link = hash[h]
    m.inhash = true
    atomicstorep(unsafe.Pointer(&hash[h]), unsafe.Pointer(m))
}

三. 问题

  1. 值接收者和指针接收者的区别: 如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。
  2. 使用指针作为方法的接收者的理由:
  1. 方法能够修改接收者指向的值。
  2. 避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。
  1. 实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法;而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。具体查看下方代码
  1. 定义了一个结构体 Gopher,它实现了两个方法,一个值接收者,一个指针接收者。在 main 函数里通过接口类型的变量调用了定义的两个函数,可以正常执行
package main
import "fmt"
type coder interface {
    code()
    debug()
}
type Gopher struct {
    language string
}
func (p Gopher) code() {
    fmt.Printf("I am coding %s language\n", p.language)
}
func (p *Gopher) debug() {
    fmt.Printf("I am debuging %s language\n", p.language)
}
func main() {
    var c coder = &Gopher{"Go"}
    c.code()
    c.debug()
}
  1. 但是把 main 函数的第一条语句换一下,运行一下,报错: ,因为 Gopher 类型并没有实现 debug 方法;而上面指针的 *Gopher 类型也没有实现 code 方法,但是因为 Gopher 类型实现了 code 方法,所以让 *Gopher 类型自动拥有了 code 方法
func main() {
    var c coder = Gopher{"Go"}
    c.code()
    c.debug()
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值