golang Interface

Interface



空接口 interface{}

空接口类型可以接收任意类型的数据,它只要记录这个数据在哪儿,是什么类型的就足够了。空接口变量数据结构如下,其中_type指向接口的动态类型元数据,data就指向接口的动态值。

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

比如下面这个例子,为e赋值以前,其中存储的_type和data都为nil。

var e interface{}

在这里插入图片描述

f, _ := os.Open("hello.txt")
e = f

如果我们把*os.File类型的变量f赋给e。那么变量e的结构如下图所示:

在这里插入图片描述

因为f本身就是个指针,所以e这里的data就等于f,动态类型就是*os.File。值得强调的是类型元数据这里是可以找到类型关联的方法元数据列表的,这一点对于理解“类型断言”至关重要。

interface{}参数赋值

interface{}类型的变量中会保存一个地址。假如存在如下的一种情况:

var e interface{}
a := "eggo"
e = a

那么e接收的参数会是下面这样的吗?

在这里插入图片描述

答案是否定的,原因是这样并不符合“Go语言中传参值拷贝”的语义。从语义上讲也应该使用a的拷贝值,而不是直接使用a。但问题是空接口类型的参数需要的只是一个数据指针,不能拷贝a的值过来,又不能拷贝a的地址过来,那拷贝谁?

解决这个问题的方式,是在编译阶段增加临时变量作为a的拷贝值(copy of a),再把copy of a的地址传给函数使用,无论之后对参数指向的数据做什么修改,都不会作用到变量a身上。这样就通过传递复制后的地址实现了传值的语义

copy of a的地址会在编译阶段转换为空接口类型,所以TypeOf接收到的参数就如下图所示。

在这里插入图片描述

非空接口

非空接口就是有方法列表的接口类型,一个变量要想赋值给一个非空接口类型,其类型必须要实现该接口要求的所有方法才行。

type iface struct {
    tab   *itab
    data  unsafe.Pointer
}

iface.data记录的是接口的动态值,所以接口要求的方法列表以及与data对应的动态类型信息一定存在itab里面。

type itab struct {
    inter  *interfacetype
    _type  *_type
    hash   uint32
    _      [4]byte
    fun    [1]uintptr 
}

itab.inter是interface的类型元数据,它里面记录了这个接口类型的描述信息,接口要求的方法列表就记录在interfacetype.mhdr这里。

type interfacetype struct {
    typ      _type
    pkgpath  name
    mhdr     []imethod
}   
  • itab._type就是接口的动态类型,也就是被赋给接口类型的那个变量的类型元数据。
  • itab.hash是从itab._type中拷贝来的,是类型的哈希值,用于快速判断类型是否相等时使用。
  • itab.fun记录的是动态类型实现的那些接口要求的方法的地址,是从方法元数据中拷贝来的,为的是快速定位到方法。如果itab._type对应的类型没有实现这个接口,则itab.fun[0]=0,这在类型断言时会用到。

非空接口赋值前后

如果我们声明一个io.ReadWriter类型的变量rw。被赋值以前,rw的data为nil,tab也为nil。

var rw io.ReadWriter

在这里插入图片描述

下面我们把一个*os.File类型的变量f,赋值给rw。

f, _ := os.Open("hello.txt")
rw = f

此时rw的动态值就是f,动态类型就是os.File。而itab.fun这个数组里记录的是os.File实现的Read、Write方法的地址。

在这里插入图片描述

下面我们再声明一个io.Writer类型的变量w,并把f赋值给w。

var w io.Writer = f

此时w的动态值和动态类型与rw相同,只是二者的接口类型元数据不同,要求的方法列表也不同罢了。

在这里插入图片描述

itab缓存

关于itab我们还要额外关注一点,既然一个非空接口类型和一个动态类型就可以确定一个itab的内容,那这个itab结构体自然是可以被接口类型与动态类型均相同的接口变量复用的。
在这里插入图片描述

实际上Go语言会把用到的itab结构体缓存起来,并且以<接口类型, 动态类型>组合为key,以*itab为value,构造一个哈希表,用于存储与查询itab信息。

在这里插入图片描述

这个哈希表与map底层的哈希表不同,其结构设计更为简便。

type itabTableType struct {
    size    uintptr             // length of entries array. Always a power of 2.
    count   uintptr             // current number of filled entries.
    entries [itabInitSize]*itab // really [size] large
}

需要一个itab时,会首先去itabTable里查找,计算哈希值时会用到接口类型(itab.inter)和动态类型(itab._type)的类型哈希值:

func itabHashFunc(inter *interfacetype, typ *_type) uintptr {
    return uintptr(inter.typ.hash ^ typ.hash)
}

如果能查询到对应的itab指针,就直接拿来使用。若没有就要再创建,然后添加到itabTable中。

了解了空接口和非空接口的数据结构,明确了接口动态值与动态类型在赋值前与赋值后的变化,接下来就可以看看“类型断言”是怎么回事儿了。

参考资料:

https://mp.weixin.qq.com/s/wUvkkHx4b6OFLRfSaiZoHw

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值