interface

interface 是一种类型,一种抽象的类型。

接口的本质就是引入一个新的中间层,调用方可以通过接口与具体实现分离,解除上下游的耦合,上层的模块不再需要依赖下层的具体模块,只需要依赖一个约定好的接口。

接口的基本实现

接口就是一个需要实现的方法列表,如下的Sayer接口(列表中只有一个say):

// Sayer 接口
type Sayer interface {
	say()
}

type dog struct {}
type cat struct {}

// dog实现了Sayer接口
func (d dog) say() {
	fmt.Println("汪汪汪")
}

// cat实现了Sayer接口
func (c cat) say() {
	fmt.Println("喵喵喵")
}

func main() {
	var x Sayer // 声明一个Sayer类型的变量x
	a := cat{}  // 实例化一个cat
	b := dog{}  // 实例化一个dog
	x = a       // 可以把cat实例直接赋值给x
	x.say()     // 喵喵喵
	x = b       // 可以把dog实例直接赋值给x
	x.say()     // 汪汪汪
}

值接收者和指针接收者实现接口的区别

从上面的代码中可以发现,使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui

此时实现Sayer接口的是*dog类型,所以不能给x传入dog类型的wangcai,此时x只能存储*dog类型的值。

一个类型实现多个接口

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。就分别定义Sayer接口和Mover接口,如下: Mover接口。

dog既可以实现Sayer接口,也可以实现Mover接口。

多个类型实现同一接口

Go语言中不同的类型还可以实现同一接口 首先定义一个Mover接口,它要求必须由一个move方法。

例如狗可以动,汽车也可以动,可以使用如下代码实现这个关系:

这个时候在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move方法就可以了。

上面的代码执行结果如下:

并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现(类似于继承)。

接口嵌套

接口与接口间可以通过嵌套创造出新的接口。

嵌套得到的接口的使用与普通接口一样,下面让cat实现animal接口:

空接口

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。

空接口类型的变量可以存储任意类型的变量。

空接口可以作为函数的参数

使用空接口实现可以接收任意类型的函数参数。

空接口作为map的值

使用空接口实现可以保存任意值的字典。

类型元数据

不管是内置类型,还是自定义类型,都有对应的类型描述信息,这就是类型元数据。而且每种类型元数据都是全局唯一的(包括所有内建类型和自定义类型),这些类型元数据共同构成了Go语言的类型系统。

类型元数据对应在源码中即是_type,含义是concrete type,即实际类型描述信息。每个类型元数据基础信息都是runtime._type结构体,作为每个类型元数据的Header,所有变量类型都是以_type为内嵌字段封装而成的结构体,编译器和运行时可以根据该元信息解析具体类型、类型名存放的位置、类型的 Hash 值等基本信息。

变量包括(type, value)两部分,这2个部分都相等,才能判断为相等,理解这一点就知道为什么有时候nil != nil了。

而 type 包括 <static type, concrete type>。static type是你在编码是看见的类型(如int、string),concrete type是runtime系统看见的类型。

slice为例了解运行时类型系统的设计

第一个typ变量_type是本slice的基本类型元数据,elem 变量 *_type 指向其存储的元素的类型元数据,如果是string类型的slice,这个指针就指向string类型的元数据。

自定义类型

自定义类型会多一个uncommontype结构体:

利用类型元数据来解释下面两种写法的区别:

  • 取别名:type MyType1 = int32

  • 自定义类型:type MyType2 int32

MyType1和int32会关联到同一个类型元数据,属于同一种类型,rune和int32就是这样的关系。

MyType2这种写法属于基于已有类型创建新类型,MyType2会自立门户,拥有自己的类型元数据,会多一个uncommontype结构体,即使MyType2相对于int32来说没有做任何改变,他们两个对应的类型元数据也已经不同了。

接口底层实现原理

根据 interface 是否包含有 method,底层实现上用两种 struct 来表示:iface 和 eface。eface 含义是 empty interface,表示不含 method 的 interface 结构。iface 至少包含一个 method,method 的具体实现存放在 itab.fun 变量里。

itab

类型断言

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:

其中:

  • x:表示类型为interface{}的变量

  • T:表示断言x可能是的类型。

该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。

举个例子:

上面的示例中如果要断言多次就需要写多个if判断,这个时候可以使用switch语句来实现:

类型断言的原理

Go语言里每种类型的类型元数据都是全局唯一的(包括所有内建类型和自定义类型),以分析 non-empty 类型的 interface 为例,其 itab 指针是用<interface type, concrete type>作为key做全局缓存的,因此判断接口变量的动态类型,只需要取出该变量元数据指针,看是否指向目标全局类型元数据指针即可。也就是说,类型断言能否成功,取决于变量的concrete type,而不是static type。

反射

反射是针对于 interface 类型来说的,静态类型在编译阶段确定其具体类型,但当变量为 interface 时,只有在运行时才能确定其具体类型,反射就是允许在运行时再去读取和修改一个变量的类型信息和值信息的机制。

interface及其pair<concrete type,value>)的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来操作存储在接口变量内部pair对的一种机制。reflect.TypeOf()读取的是concrete type,reflect.ValueOf()读取的是value,可以再调用返回变量提供的Set方法来修改它的value

典型场景:json反序列化,接收的是文本数据,是不知道对应在 go 语言中是什么数据类型的,需要通过反射机制在运行时阶段来实现文本数据解析到 go 语言对应数据类型。下面是通过reflect读取字段信息的例子:

优缺点

反射可以大大提高程序的灵活性,使得interface有更大的发挥空间。但是:

  1. 反射中的类型错误会在运行时导致panic。

  2. 大量使用反射的代码通常难以理解。

  3. 降低了代码的运行效率,因为反射出变量的类型需要额外开销。

Last updated

Was this helpful?