Go 接口 interface

2023-04-13 09:17:01 阅读:1036 评论:0 点赞:0
所属分类: Go 语言学习笔记

一、概念

Go 语言中的接口是一组方法的签名,它是 Go 语言的重要组成部分。接口的本质是引入一个新的中间层,调用方可以通过接口与具体实现分离,解除上下游的耦合,上层的模块不再需要依赖下层的具体模块,只需要依赖一个约定好的接口。
79c36e33-c1f8-4441-bddc-199f12a26805

即我们只需要关注怎么使用,而不需要关注具体实现。

计算机科学中的接口是比较抽象的概念,但是编程语言中接口的概念就更加具体。Go 语言中的接口是一种内置的类型,它定义了一组方法的签名。

二、定义接口

在 Go 中通过 interface 关键词来定义接口,在接口中我们只能定义方法签名,不能包含成员变量,例如:

type error interface {
	Error() string
}

2.1 实现

若我们需要实现某个接口,我们只需要实现它定义的方法签名即可:

type RPCError struct {
	Code    int64
	Message string
}

func (e *RPCError) Error() string {
	return fmt.Sprintf("%s, code=%d", e.Message, e.Code)
}

提示

在 Go 语言中接口的实现都是隐式的,我们只需要实现接口的方法就是实现了对应的接口。

2.2 类型

接口是 Go 中的一种类型,它能够出现在变量的定义、函数的入参和返回值中并对它们进行约束,不过 Go 语言中有两种略微不同的接口,一种是带有一组方法的接口,另一种是不带任何方法的 interface{}
1cf73485-6daf-4ecb-906a-5f303c56941f

runtime.iface 是第一种接口,runtime.eface 表示第二种不包含任何方法的接口 interface{},两种接口虽然都使用 interface 声明,但是由于后者在 Go 语言中很常见,所以在实现时使用了特殊的类型。

package main

func main() {
	type Test struct{}
	v := Test{}
	Print(v)
}

func Print(v interface{}) {
	println(v)
}

注意

interface{} 类型不是任意类型,在调用 Print 函数时会对参数 v 进行类型转换,将原来的 Test 类型转换成 interface{} 类型。

2.3 指针和接口

当使用指针实现接口时,只有指针变量才会实现该接口;使用结构体实现接口时,指针类型和结构体类型都会实现该接口。

type Duck interface {
	Quack()
}

type Cat struct{}

func (c *Cat) Quack() {
	fmt.Println("meow")
}

func main() {
	var c Duck = Cat{}
	c.Quack()
}

主要原因是 Go 函数传递参数是值传递,对于指针则是拷贝一个新的指针,但是指向还是原结构体,所以可以隐式的对变量解引用获取指针。
843274f0-f92b-4452-8bec-b14e66b5505c

三、数据结构

type eface struct { // 16 字节
	_type *_type
	data  unsafe.Pointer
}

type iface struct { // 16 字节
	tab  *itab
	data unsafe.Pointer
}

3.1 类型结构体 _type

runtime._type 是 Go 运行时表现:

type _type struct {
	size       uintptr
	ptrdata    uintptr
	hash       uint32
	tflag      tflag
	align      uint8
	fieldAlign uint8
	kind       uint8
	equal      func(unsafe.Pointer, unsafe.Pointer) bool
	gcdata     *byte
	str        nameOff
	ptrToThis  typeOff
}

size 字段存储类型占用的内存空间
hash 快速定位类型是否相等
equal 字段用于判断当前类型的多个对象是否相等,减少了 Go 二进制包的大小。

3.2 结构体 itab

runtime.itab 结构体是接口类型的核心组成部分,占用 32 字节,是接口类型和具体类型的组合:

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

inter_type 用来表示字段类型
hash_type.hash 的拷贝,可以使用该字段来快速转换为具体类型或者用于类型判断。
fun 是一个动态大小的数组,它是一个用于动态派发的虚函数表,存储了一组函数指针。虽然该变量被声明成大小固定的数组,但是在使用时会通过原始指针获取其中的数据,所以 fun 数组中保存的元素数量是不确定的

四、具体类型和接口类型相互转换

4.1 结构体指针

对于结构体指针转为接口类型时,底层使用 runtime.iface 结构体表示。其中 itab 用来表示结构体与接口的关系,data 指针则是指向数据的指针。
5f891720-9782-4c31-aaf3-ea0b6c8df348
调用接口类型对应的方法时,需要去调用具体实现的方法,这种行为属于动态派发的方法调用;为了减少额外的开销 Go 编译时会自动转换为对目标方法的直接调用,例如:Duck.Quack() 编译期间自动转换为 *Cat.Quack()

4.2 结构体类型

同结构体指针,还是一样将结构体转换成对应的接口的类型,然后调用接口的方法。具体过程:
1、初始化结构体
2、将结构体类型转换为接口类型
通过 runtime.convT2I 函数将结构体转换成 iface 类型的接口,调用接口方法时就是调用 runtime.itab 中实现对应方法的指针。

4.3 类型断言

通过类型断言,我们可以将一个接口类型转换成具体类型:

非空接口 iface

func main() {
	var c Duck = &Cat{Name: "draven"}
	switch c.(type) {
	case *Cat:
		cat := c.(*Cat)
		cat.Quack()
	}
}

通过比较目标类型的 hash 与接口变量的 itab.hash 若相等直接进行类型转换,然后调用具体的实现。

空接口 eface

func main() {
	var c interface{} = &Cat{Name: "draven"}
	switch c.(type) {
	case *Cat:
		cat := c.(*Cat)
		cat.Quack()
	}
}

转换逻辑与 iface 同理。

五、多态派发

动态派发(Dynamic dispatch)是在运行期间选择具体多态操作(方法或者函数)执行的过程,它是面向对象语言中的常见特性。如果编译期间不能确认接口的类型,Go 语言会在运行期间决定具体调用该方法的哪个实现。

func main() {
	var c Duck = &Cat{Name: "draven"}
	c.Quack()
	c.(*Cat).Quack()
}

使用结构体类型实现接口比结构体指针类型实现接口开销高,我们应当尽量避免使用结构体类型实现接口。

附录

参考原文《Go语言设计与实现》

永不言弃

职业:后端开发工程师
学校:重庆师范大学
城市:重庆
文章:169
好吧,不知道说点什么...

登录逐梦笔记

注册逐梦笔记

已有账号?