interface和interface{} | 青训营笔记

123 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天

理解interface和interface{}的区别

interface

go面向对象的时候,interface是必不可少的,interface是golang最重要的特性之一,实现多态。

Interface类型可以定义一组方法,但是这些不需要实现。并且interface不能包含任何变量。

基本语法

定义一个接口

type xxx interface { 
    // 声明方法 
    method1(参数列表) 返回值列表 
    method2(参数列表) 返回值列表
}

实现一个接口

func (t 自定义类型)method1(参数列表)返回值列表 { 
    //方法实现 
} 
func (t 自定义类型)method2(参数列表)返回值列表 { 
    //方法实现 
}

interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil。

空接口interface{}没有任何方法,所以所有类型都实现了空接口,即我们可以把任何一个变量赋给空接口类型。

底层实现

Go的interface源码在Golang源码的runtime目录中。

Go的interface是由两种类型来实现的:iface和eface。

iface

结构

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

eface

空接口,不包含任何方法的接口,下面会详细介绍一下空接口interface{}

type xxxx interface { 

}

结构

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

相比 iface,eface 就比较简单了。只维护了一个 _type 字段,表示空接口所承载的具体的实体类型。data 描述了具体的值。

image.png

为什么go删除了接口,实现了的也可以用?这里就可以解释到java中接口和go中接口的一个不同

侵入式与非侵入式的理解

侵入式

你的代码里已经嵌入了别的代码,这些代码可能是你引入过的框架,也可能是你通过接口继承得来的,比如:java中的继承,必须显示的表明我要继承那个接口,这样你就可以拥有侵入代码的一些功能。所以我们就称这段代码是侵入式代码。

优点:通过侵入代码与你的代码结合可以更好的利用侵入代码提供给的功能。

缺点:框架外代码就不能使用了,不利于代码复用。依赖太多重构代码太痛苦了。

非侵入式

正好与侵入式相反,你的代码没有引入别的包或框架,完完全全是自主开发。比如go中的接口,不需要显示的继承接口,只需要实现接口的所有方法就叫实现了该接口,即便该接口删掉了,也不会影响我,所有go语言的接口数非侵入式接口;再如Python所崇尚的鸭子类型。

优点:代码可复用,方便移植。非侵入式也体现了代码的设计原则:高内聚,低耦合。

缺点:无法复用框架提供的代码和功能。

侵入式和非侵入式的总结

  1. 侵入式通过 implements 把实现类与具体接口绑定起来了,因此有了强耦合;
  2. 假如修改了接口方法,则实现类方法必须改动;
  3. 假如类想再实现一个接口,实现类也必须进行改动;
  4. 后续实现此接口的类,必须了解相关的接口;
  5. Go语言非侵入式的方式很好地解决了这几个问题,只要实现了实现了与接口相同的方法,就实现了这个接口。随着代码量的增加,根本不需要的关心实现了哪些接口,不需要刻意去先定义接口再实现接口的固定模式,在原有类新增实现接口时,不需要更改类,做到低侵入式、低耦合开发的好处。

interface{}

空接口可用于保存任何数据,它可以是一个有用的参数,因为它可以使用任何类型。要理解空接口如何工作以及如何保存任何类型,我们首先应该理解空接口名称背后的概念。

在1.18以前泛型没出来之前空接口也可以有替代他的这样的功能,但是因为Go是强类型语言,用interface{} 类型会打破强类型的约束,使程序暴露更多的错误。

所以空接口作为参数的函数可以接受任何类型,Go将转换为接口类型以满足这个函数

以下是 Russ 在 2009 年画的示意图,当时 runtime 包还是用 C 语言编写:

image.png

现在runtime包用Go编写,只换个语言但是结构是没有变的。

打印空接口可以知道还是两个地址,一个代表类型,一个代表值。

底层结构

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

他和iface的区别在于,他拥有的不再是*itab类型的数据,而是_type类型的数据。是因为,eface面向的是空接口, 面向空接口的时候只要关注类型指向就可以了,也感觉有点像java中的Object。

空接口可以做那种转换

转换

错误演示

func main() {
    var i int8 = 1 read(i) 
} 

//go:noinline 
func read(i interface{}) { 
    n := i.(int16) 
    println(n) 
}

这样会panic异常

panic: interface conversion: interface {} is int8, not int16 

goroutine 1 [running]: 
main.read(0x10592e0, 0x10be5c1) 
main.go:10 +0x7d 
main.main() 
main.go:5 +0x39 
exit status 2

具体原因就得看看生成汇编过程了

生成 asm[4] 代码,以便查看 Go 执行的检查:

image.png

查阅相关资料后

主要有以下几个步骤:

  • 步骤 1:比较 int16 类型与 空接口 的内部类型:比较(指令 CMPQ)int16 类型(加载有效地址 LEAQ(Load Effective Address)到空接口的内部类型(从空接口 MOVQ 的内存段读取 48 字节偏移量的内存的指令)
  • step 2:JNE 指令,即不相等则跳转指令(Jump if Not Equal),会跳转到已生成的处理错误的指令,这些指令将在步骤中处理错误 3
  • 步骤 3:代码将 panic 并生成我们上面看到的错误信息
  • 步骤 4:这是错误指令的结束。此特定指令由显示指令的错误消息引用:main.go:10 +0x7d

任何从空接口内部类型的转换,都应该在原始类型转换完成后进行。这种转换为空接口,然后转换回原始类型会导致程序损耗。

注:这句话是说,比如 interface{} 存了一个 int16; 需要转换为 int32 时,不能直接 interface{}-> int32;应该是 interface{}->int16->int32,这也是上面的例子 panic 的原因

小结

开始对接口是怎么实现的有点好奇,然后主要了解了一下interface的底层,还有和interface{}的区别