1. 接口的基本使用

  golang中的interface本身是一种类型,它代表一个方法的集合。任何类型只要实现了接口中声明的所有方法,那么该类就实现了该接口。与其他语言不同,golang并不需要显式声明类型实现了某个接口,而是由编译器和runtime进行检查。接口解除了类型依赖, 有助于减少可视方法,屏蔽内部结构和实现细节。

声明

 type 接口名 interface{
方法1
方法2
...
方法n
}
type 接口名 interface {
已声明接口名1
...
已声明接口名n
}
type iface interface{
tab *itab
data unsafe.Pointer
}

  接口变量默认值是nil,如果一个接口不包含任何方法,那么它就是一个空接口,则它可被赋值为任何模型,任何类型也都能转换成empty interface。

  接口的值简单来说,是由两部分组成的,就是类型和数据,判断两个接口是相等,就是看他们的这两部分是否相等;另外类型和数据都为nil才代表接口是nil。实现接口时,一般先实现类型,再抽象出所需接口。

  var a interface{}
var b interface{} = (*int)(nil)
fmt.Println(a == nil, b == nil) // true false 

接口自身也是一种结构类型,只是编译器对其做了很多限制:

  • 不能有字段
  • 不能定义自己的方法
  • 只能声明方法,不能实现
  • 可嵌入其他接口类型
 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) // 输出"小明"
}

  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哈希的方法
_ []byte
fun []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 = 

 // 注意:如果更改这些字段,请在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 == {
x = unsafe.Pointer(&zeroVal[])
} else {
x = mallocgc(, 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()
}
反汇编:
  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.*+(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) == {
throw("internal error - misuse of itab")
} // 简单的情况
if typ.tflag&tflagUncommon == {
if canfail {
return nil
}
name := inter.typ.nameOff(inter.mhdr[].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)-)*sys.PtrSize, , &memstats.other_sys))
m.inter = inter
m._type = typ
m.init()
itabAdd(m)
unlock(&itabLock)
finish:
if m.fun[] != {
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 -
h := itabHashFunc(inter, typ) & mask
for i := uintptr(); ; 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 := (*[ << ]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
j :=
imethods:
for k := ; 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[]), uintptr(k)*sys.PtrSize)) = ifn
}
continue imethods
}
}
}
// didn't find method
m.fun[] =
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 != {
throw("malloc deadlock")
} t := itabTable
if t.count >= *(t.size/) { // 75% 负载系数
// 增长哈希表。
        // t2 = new(itabTableType)+一些其他条目我们撒谎并告诉malloc我们想要无指针的内存,因为所有指向的值都不在堆中。
t2 := (*itabTableType)(mallocgc((+*t.size)*sys.PtrSize, nil, true))
t2.size = t.size * // 复制条目。
        //注意:在复制时,其他线程可能会寻找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 -
h := itabHashFunc(m.inter, m._type) & mask
for i := uintptr(); ; 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插入到新表中。

go中的数据结构接口-interface的更多相关文章

  1. java中的接口interface

    关于接口 接口描述了实现了它的类拥有什么功能.因为Java是强类型的,所以有些操作必须用接口去约束和标记.接口作为类的能力的证明,它表明了实现了接口的类能做什么. 类似与class,interface ...

  2. 介绍Unreal Engine 4中的接口(Interface)使用C++和蓝图

    这个教程是从UE4 Wiki上整理而来. 在C++中直接使用Interface大家应该很熟悉.只是简单先定义一个个有虚函数的基类,然后在子类中实现相应的虚函数.像这样的虚函数的基类一般概念上叫接口.那 ...

  3. php中的抽象类(abstract class)和接口(interface)

    一. 抽象类abstract class 1 .抽象类是指在 class 前加了 abstract 关键字且存在抽象方法(在类方法 function 关键字前加了 abstract 关键字)的类. 2 ...

  4. php中的接口interface

    * 接口 * 1.使用关键字:interface * 2.类是对象的模板,接口是类的模板 * 3.接口看作是一个特殊的类 * 4.接口中的方法,只声明不实现,与抽象类一样 * 5.接口中的方法必须是p ...

  5. go语言中的接口interface

    package main; import "fmt" //接口interface //接口是一个或多个方法签名的集合 //只要某个类型拥有该接口的所有方法签名,即算实现该接口. / ...

  6. C#中抽象类(abstract)和接口(interface)的实现

    抽象类 抽象方法是没有代码实现的方法,使用abstract关键字修饰: 抽象类是包含0到多个抽象方法的类,其不能实例化.含有抽象方法的类必须是抽象类,抽象类中也可以包含非抽象方法: 重写抽象类的方法用 ...

  7. Objective-C 中的协议(@protocol)和接口(@interface)的区别

    Objective-C 中的协议(@protocol),依照我的理解,就是C#, Java, Pascal等语言中的接口(Interface).协议本身不实现任何方法,只是声明方法,使用协议的类必须实 ...

  8. java中接口interface和private私有内部类怎样一块配合着用?

    3.接口interface和private内部类协同工作[新手可忽略不影响继续学习]马克-to-win:由于是private内部类,外面无法访问甚至无法看到你编的源代码(如果在不同的包中),非常安全. ...

  9. Go语言学习笔记(四)结构体struct & 接口Interface & 反射

    加 Golang学习 QQ群共同学习进步成家立业工作 ^-^ 群号:96933959 结构体struct struct 用来自定义复杂数据结构,可以包含多个字段(属性),可以嵌套: go中的struc ...

随机推荐

  1. JDK8 Optional操作学习

    介绍 Optional是JDK8中提供用于包含未知对象的工具类,即可以利用Optional包装对象来避免繁琐的空指针检查,以及NullPointException的处理,在Optional中,用val ...

  2. PMP(第六版)中的控制账户、规划包、工作包

    PMP(第六版)中的控制账户.规划包.工作包 控制账户是一个管理控制点,在该控制点上,把范围.预算和进度加以整合,并与挣值比较,以测量绩效.控制账户拥有2个或以上的工作包,但每个工作包只与一个控制账户 ...

  3. [JZOJ100026]【NOIP2017提高A组模拟7.7】图

    Description 有一个n个点n条边的有向图,每条边为<i,f(i),w(i)>,意思是i指向f(i)的边权为w(i)的边,现在小A想知道,对于每个点的si和mi. si:由i出发经 ...

  4. go-异常处理-error-panic-recover

    Go语言的函数可以一次返回多个结果.这就为我们温和地报告错误提供了语言级别的支持. func readFile(path string) ([]byte, error) { file, err := ...

  5. php和apache的关系是什么?

    例如我们在自己的电脑里练习如何做网站的时候,我们需要搭建一个本地环境,这个本地环境就是PHP+APACHE + MYSQL的环境,这样就可以让我们的电脑拥有PHP要求的运行环境了.那么php和apac ...

  6. 【Autofac打标签模式】Aspect拦截器

    [ Autofac打标签模式]开源DI框架扩展地址: https://github.com/yuzd/Autofac.Annotation/wiki 前提条件 自己new一个对象不能实现拦截器功能,必 ...

  7. 百万年薪python之路 -- 软件的开发规范

    一. 软件的开发规范 什么是开发规范?为什么要有开发规范呢? 你现在包括之前写的一些程序,所谓的'项目',都是在一个py文件下完成的,代码量撑死也就几百行,你认为没问题,挺好.但是真正的后端开发的项目 ...

  8. 关于到美国学习cs的亲身感受,希望对你们有所帮助

    1.能否向各位寄托天下的朋友们简单介绍一下你自己?比如你国内的学校(或者什么档次),哪年申请出国的,什么专业,硕士还是博士,在美国的学校(或者什么档次),以及留学经历(毕业时间),现在状态(学生?博后 ...

  9. Kafka、Redis和其它消息组件比较

    Kafka作为时下最流行的开源消息系统,被广泛地应用在数据缓冲.异步通信.汇集日志.系统解耦等方面.相比较于RocketMQ等其他常见消息系统,Kafka在保障了大部分功能特性的同时,还提供了超一流的 ...

  10. 匹马行天下之思维决定高度篇——道道道,学习Java之道

    致Java星球的程序员兄弟们的一封信 亲爱的Java星球的程序员兄弟们: 你们好!我是来自地球的一名Java程序员,首先我代表地球人对贵星球的高司令来到地球传授Java语言,造福了全人类,造福了整个地 ...